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::marker::PhantomData;
14use std::sync::Arc;
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/// Per SRD-67 §"Decision 4". `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 (per SRD-13e §1.2).
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 wires the parent's `SharedCell` into
142/// the child's `X` input slot via `materialize_wiring_from_outer`. 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, hashed,
162/// inspected, 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 pub(crate) contract: ScopeContract<M>,
168 pub(crate) context: SourceContext,
169 pub(crate) consumers: Vec<RegisteredPullConsumer>,
170 /// Rule 2 write-through bindings — child exports rewritten
171 /// at finalize to feed parent `shared` cells. Empty for the
172 /// vast majority of modules; populated only when the parent
173 /// has a `shared` export with a name the child redefines.
174 pub(crate) write_throughs: Vec<WriteThroughBinding>,
175 /// Diagnostics emitted during finalize (warnings, etc.). Free-
176 /// form strings; downstream tooling can surface them.
177 pub(crate) diagnostics: Vec<String>,
178 pub(crate) _module: PhantomData<fn() -> M>,
179}
180
181impl<M> ScopeModule<M> {
182 /// The imports the module declares.
183 pub fn imports(&self) -> &[ImportSpec] {
184 &self.imports
185 }
186
187 /// The exports the module declares.
188 pub fn exports(&self) -> &[ExportSpec] {
189 &self.exports
190 }
191
192 /// The body's compiled program.
193 pub fn program(&self) -> &Arc<PolydatProgram> {
194 &self.program
195 }
196
197 /// The contract the module was built against.
198 pub fn contract(&self) -> &ScopeContract<M> {
199 &self.contract
200 }
201
202 /// Where the module comes from.
203 pub fn context(&self) -> &SourceContext {
204 &self.context
205 }
206
207 /// The pull consumers registered on the module's outputs.
208 pub fn consumers(&self) -> &[RegisteredPullConsumer] {
209 &self.consumers
210 }
211
212 /// Diagnostics the build recorded.
213 pub fn diagnostics(&self) -> &[String] {
214 &self.diagnostics
215 }
216
217 /// Rule 2 write-through bindings produced by the builder for
218 /// child exports that collide with a parent `shared` export.
219 pub fn write_throughs(&self) -> &[WriteThroughBinding] {
220 &self.write_throughs
221 }
222}
223
224impl<M> std::fmt::Debug for ScopeModule<M> {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 f.debug_struct("ScopeModule")
227 .field("imports", &self.imports)
228 .field("exports", &self.exports)
229 .field("context", &self.context)
230 .field("consumer_count", &self.consumers.len())
231 .field("write_throughs", &self.write_throughs)
232 .field("diagnostics", &self.diagnostics)
233 .finish()
234 }
235}