Skip to main content

nmbrs_runtime/
scope_kernel.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! A node of nmbrs's scope tree: a kernel on the fiber engine, beside the
5//! interpreter program scope synthesis reads.
6//!
7//! polydat splits a scope in two (native_scope_trees.md §2): the analysis
8//! program — which inputs are coordinates, what a child re-emits from its
9//! parent, checkpoint identity, the describe views — and the kernel that
10//! runs, on any engine. A [`ScopeKernel`] keeps both. Synthesis reads
11//! [`ScopeKernel::program`]; everything that evaluates goes through the
12//! engine-neutral `Kernel` the type dereferences to, and children are
13//! bound under it with `bind_under` / `instantiate_under`.
14//!
15//! The running kernel is on [`crate::fiber_engine::fiber_engine`] when the
16//! engine's image lists the program's inputs and outputs in the same
17//! order (so an index resolved on the program drives it), and on the
18//! interpreter otherwise.
19
20use std::sync::Arc;
21
22use polydat::Kernel;
23use polydat::ast::Value;
24use polydat::kernel::interp::{KernelLookup, Lookup};
25use polydat::kernel::{KernelProgram, PolydatKernel, PolydatProgram};
26
27use crate::fiber_engine::OpTemplateModule;
28
29/// A scope kernel and the interpreter program it stands for.
30pub struct ScopeKernel {
31    kernel: Box<dyn Kernel>,
32    /// The analysis program; positions agree with `kernel`'s.
33    program: Arc<PolydatProgram>,
34    /// The program `kernel` runs, which an iteration of this scope binds.
35    image: Arc<dyn KernelProgram>,
36    /// The module a source-built scope finalized to: another instance
37    /// carries its write-throughs.
38    module: Option<Arc<OpTemplateModule>>,
39}
40
41impl std::fmt::Debug for ScopeKernel {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("ScopeKernel")
44            .field("engine", &self.kernel.engine())
45            .field("outputs", &self.program.output_names())
46            .finish()
47    }
48}
49
50impl std::ops::Deref for ScopeKernel {
51    type Target = dyn Kernel;
52    fn deref(&self) -> &Self::Target {
53        self.kernel.as_ref()
54    }
55}
56
57impl std::ops::DerefMut for ScopeKernel {
58    fn deref_mut(&mut self) -> &mut Self::Target {
59        self.kernel.as_mut()
60    }
61}
62
63impl From<PolydatKernel> for ScopeKernel {
64    /// An interpreter kernel as a scope: a kernel standing outside the
65    /// tree's construction (a test fixture, a probe) that a scope is
66    /// bound under.
67    fn from(kernel: PolydatKernel) -> Self {
68        let program = kernel.program().clone();
69        Self {
70            image: program.clone(),
71            program,
72            kernel: Box::new(kernel),
73            module: None,
74        }
75    }
76}
77
78impl ScopeKernel {
79    /// A root scope compiled from source: `interpreter` is the compiled
80    /// kernel, and `image` the fiber engine's program for the same source
81    /// and options, when it can stand in for `interpreter`'s program
82    /// ([`crate::fiber_engine::source_image`]).
83    pub fn root(interpreter: PolydatKernel, image: Option<Arc<dyn KernelProgram>>) -> Self {
84        match image {
85            Some(image) => Self {
86                program: interpreter.program().clone(),
87                kernel: image.clone().create_kernel(),
88                image,
89                module: None,
90            },
91            None => Self::from(interpreter),
92        }
93    }
94
95    /// A root scope compiled from `source` with default options
96    /// ([`crate::bindings::compile_scope_kernel`]).
97    pub fn compile(source: &str) -> Result<Self, String> {
98        crate::bindings::compile_scope_kernel(source, &Default::default())
99    }
100
101    /// A child scope built from source matter under `parent` — the
102    /// any-engine form of `build_subscope` (native_scope_trees.md §5):
103    /// the matter finalizes to a module, which is instantiated under
104    /// `parent`. Strict mode refuses a const that silently fell through
105    /// to an outer value, as `build_subscope` does.
106    pub fn build_under(parent: &dyn Kernel, matter: SourceMatter) -> Result<Self, String> {
107        use polydat::kernel::subcontext::{
108            ContractViolation, RootMarker, SourceContext, SubcontextBuilder,
109        };
110        let SourceMatter {
111            label,
112            body,
113            inherited_outputs,
114            options,
115            result_bindings,
116        } = matter;
117        let strict = options.strict;
118        let mut builder: SubcontextBuilder<RootMarker> = SubcontextBuilder::under(parent);
119        builder
120            .context(SourceContext::new(label.clone()))
121            .mark_inherited_outputs(inherited_outputs)
122            .with_compile_options(options)
123            .body(if strict {
124                declare_chain_coordinates(parent, body)
125            } else {
126                body
127            });
128        if let Some(src) = result_bindings {
129            builder
130                .add_result_bindings(&src)
131                .map_err(|e| e.to_string())?;
132        }
133        let module = Arc::new(builder.finalize().map_err(|e| e.to_string())?);
134        let program = module.program().clone();
135        let image = crate::fiber_engine::module_image(&module, &label)
136            .unwrap_or_else(|| program.clone() as Arc<dyn KernelProgram>);
137        let kernel = module
138            .instantiate_under(parent, image.engine(), &[])
139            .map_err(|e| e.to_string())?;
140        let scope = Self {
141            kernel,
142            program,
143            image,
144            module: Some(module),
145        };
146        if strict {
147            let fell_through = scope.silent_fall_throughs();
148            if !fell_through.is_empty() {
149                return Err(ContractViolation::StrictNonePropagation {
150                    bindings: fell_through,
151                    site: SourceContext::new(label),
152                }
153                .to_string());
154            }
155        }
156        Ok(scope)
157    }
158
159    /// [`Self::build_under`] a parent scope, then write each of the
160    /// parent's input values into the child's input of the same name —
161    /// how every synthesized scope (phase, for_each, do-loop, op
162    /// template) is built. A value the child's input refuses is an error.
163    pub fn synthesize_under(parent: &ScopeKernel, matter: SourceMatter) -> Result<Self, String> {
164        let mut child = Self::build_under(parent.kernel(), matter)?;
165        polydat::kernel::propagate_inputs(parent.kernel(), child.kernel_mut())
166            .map_err(|e| e.to_string())?;
167        Ok(child)
168    }
169
170    /// Another instance of this scope's program under `parent`, with
171    /// `bindings` written into its inputs first — the any-engine form of
172    /// `for_iteration(canonical, parent, bindings)`, and of
173    /// `build_subscope` with program matter.
174    pub fn bind_under(
175        &self,
176        parent: &dyn Kernel,
177        bindings: &[(String, Value)],
178    ) -> Result<Self, String> {
179        let kernel = match &self.module {
180            Some(module) => module.instantiate_under(parent, self.image.engine(), bindings),
181            None => polydat::kernel::bind_under(parent, self.image.clone(), bindings),
182        }
183        .map_err(|e| e.to_string())?;
184        Ok(self.with_kernel(kernel))
185    }
186
187    /// A copy of this scope with its state — inputs, outputs, cells
188    /// shared (native_scope_trees.md §4): a fiber's starting kernel, an
189    /// activation scope, a probe that leaves this one undisturbed.
190    pub fn fork(&self) -> Self {
191        self.with_kernel(self.kernel.fork())
192    }
193
194    fn with_kernel(&self, kernel: Box<dyn Kernel>) -> Self {
195        Self {
196            kernel,
197            program: self.program.clone(),
198            image: self.image.clone(),
199            module: self.module.clone(),
200        }
201    }
202
203    /// The interpreter program this scope stands for: what synthesis and
204    /// checkpoint identity read.
205    pub fn program(&self) -> &Arc<PolydatProgram> {
206        &self.program
207    }
208
209    /// The program the running kernel is on.
210    pub fn image(&self) -> &Arc<dyn KernelProgram> {
211        &self.image
212    }
213
214    /// The module a source-built scope finalized to, from which each
215    /// fiber instantiates its own kernel with the module's write-throughs.
216    pub fn module(&self) -> Option<&Arc<OpTemplateModule>> {
217        self.module.as_ref()
218    }
219
220    /// The running kernel.
221    pub fn kernel(&self) -> &dyn Kernel {
222        self.kernel.as_ref()
223    }
224
225    /// The running kernel, for a holder of a bare `dyn Kernel` — an
226    /// adapter's canonical kernel. Its `program_id` is this scope's.
227    pub fn into_kernel(self) -> Box<dyn Kernel> {
228        self.kernel
229    }
230
231    /// The running kernel, mutably.
232    pub fn kernel_mut(&mut self) -> &mut dyn Kernel {
233        self.kernel.as_mut()
234    }
235
236    /// A name's value in this scope without evaluating anything: a
237    /// const's value, an input, a value the build folded. A computed
238    /// output is not a scope value, before or after a pull, on any
239    /// engine; [`Self::pull_value`] evaluates one.
240    pub fn lookup(&self, name: &str) -> Option<Value> {
241        KernelLookup::new(self.kernel.as_ref()).lookup(name)
242    }
243
244    /// A name's value, evaluating a computed output on a fork so this
245    /// scope's state is undisturbed: [`Self::lookup`] first, then a pull.
246    pub fn pull_value(&self, name: &str) -> Option<Value> {
247        self.lookup(name).or_else(|| {
248            let idx = self.program.output_index(name)?;
249            Some(self.kernel.fork().pull_at(idx))
250        })
251    }
252
253    /// The values this scope's inputs hold, by name, for writing into
254    /// other kernels: every input with a value except a const's slot,
255    /// which only initialization writes (each kernel the values go into
256    /// initializes its own consts from them).
257    pub fn scope_values(&self) -> Vec<(String, Value)> {
258        self.program
259            .input_names()
260            .into_iter()
261            .enumerate()
262            .filter(|(i, _)| self.program.input_kind(*i) != Some(polydat::kernel::InputKind::Const))
263            .filter_map(|(i, name)| match self.kernel.input_value_at(i) {
264                Some(Value::None) | None => None,
265                Some(value) => Some((name, value)),
266            })
267            .collect()
268    }
269
270    /// The consts whose expression evaluated to none, so their value is
271    /// the outer scope's: each const's expression output
272    /// (`__init_<name>`) pulled on a fork.
273    fn silent_fall_throughs(&self) -> Vec<String> {
274        let mut probe: Option<Box<dyn Kernel>> = None;
275        let inits = self.kernel.const_inits();
276        self.program
277            .const_outputs()
278            .into_iter()
279            .filter(|name| {
280                let own = inits
281                    .iter()
282                    .find(|c| c.name == *name)
283                    .map_or(*name, |c| c.source.as_str());
284                let Some(idx) = self.program.output_index(own) else {
285                    return false;
286                };
287                let probe = probe.get_or_insert_with(|| self.kernel.fork());
288                matches!(probe.pull_at(idx), Value::None)
289            })
290            .map(str::to_string)
291            .collect()
292    }
293}
294
295/// What a holder of a shared scope accepts: a scope kernel, one already
296/// shared, or an interpreter kernel standing in for a scope.
297pub trait IntoSharedScope {
298    /// The scope, shared.
299    fn into_shared_scope(self) -> Arc<ScopeKernel>;
300}
301
302impl IntoSharedScope for ScopeKernel {
303    fn into_shared_scope(self) -> Arc<ScopeKernel> {
304        Arc::new(self)
305    }
306}
307
308impl IntoSharedScope for Arc<ScopeKernel> {
309    fn into_shared_scope(self) -> Arc<ScopeKernel> {
310        self
311    }
312}
313
314impl IntoSharedScope for PolydatKernel {
315    fn into_shared_scope(self) -> Arc<ScopeKernel> {
316        Arc::new(ScopeKernel::from(self))
317    }
318}
319
320impl Lookup for ScopeKernel {
321    fn lookup(&self, name: &str) -> Option<Value> {
322        ScopeKernel::lookup(self, name)
323    }
324
325    fn ledger(&self) -> &Arc<polydat::kernel::CompileLedger> {
326        self.kernel.ledger()
327    }
328}
329
330/// Source matter for [`ScopeKernel::build_under`]: what
331/// `PolydatMatter::builder().source(..)` carried.
332pub struct SourceMatter {
333    /// The scope's label, for diagnostics.
334    pub label: String,
335    /// The scope's body.
336    pub body: polydat::kernel::subcontext::BodyFragment,
337    /// Outputs the scope re-emits from its parent rather than declares.
338    pub inherited_outputs: Vec<String>,
339    /// Compile options.
340    pub options: polydat::kernel::subcontext::CompileOptions,
341    /// `result:` bindings, when the scope has any.
342    pub result_bindings: Option<String>,
343}
344
345impl SourceMatter {
346    /// Matter from Polydat source text.
347    pub fn source(
348        label: impl Into<String>,
349        source: impl Into<String>,
350        options: polydat::kernel::subcontext::CompileOptions,
351    ) -> Self {
352        Self {
353            label: label.into(),
354            body: polydat::kernel::subcontext::BodyFragment::PolydatSource(source.into()),
355            inherited_outputs: Vec::new(),
356            options,
357            result_bindings: None,
358        }
359    }
360
361    /// Matter from built statements (SRD-84 graph matter).
362    pub fn statements(
363        label: impl Into<String>,
364        statements: Vec<polydat::dsl::ast::Statement>,
365        options: polydat::kernel::subcontext::CompileOptions,
366    ) -> Self {
367        Self {
368            label: label.into(),
369            body: polydat::kernel::subcontext::BodyFragment::Statements(statements),
370            inherited_outputs: Vec::new(),
371            options,
372            result_bindings: None,
373        }
374    }
375
376    /// Mark `names` as re-emitted from the parent.
377    pub fn inherited(mut self, names: Vec<String>) -> Self {
378        self.inherited_outputs = names;
379        self
380    }
381
382    /// Attach `result:` bindings.
383    pub fn results(mut self, source: impl Into<String>) -> Self {
384        self.result_bindings = Some(source.into());
385        self
386    }
387}
388
389/// A synthesized scope's source with its coordinates declared, for strict
390/// mode. Strict mode compiles no source without an explicit `input`
391/// declaration, and a synthesized scope's coordinates are its parent's,
392/// positioned through the kernel chain: what non-strict inference gives a
393/// scope from the names it leaves unbound. A body that declares its own
394/// inputs, or a parent with no coordinates, is left as it is; the
395/// declarations follow any leading `pragma` lines.
396fn declare_chain_coordinates(
397    parent: &dyn Kernel,
398    body: polydat::kernel::subcontext::BodyFragment,
399) -> polydat::kernel::subcontext::BodyFragment {
400    use polydat::kernel::subcontext::BodyFragment;
401    let BodyFragment::PolydatSource(source) = body else {
402        return body;
403    };
404    let coords = parent.coord_count();
405    if coords == 0 || source.lines().any(|l| l.trim_start().starts_with("input ")) {
406        return BodyFragment::PolydatSource(source);
407    }
408    let declarations: String = parent.input_names()[..coords]
409        .iter()
410        .map(|name| format!("input {name}: u64\n"))
411        .collect();
412    let pragmas: String = source
413        .lines()
414        .take_while(|l| l.trim_start().starts_with("pragma ") || l.trim().is_empty())
415        .map(|l| format!("{l}\n"))
416        .collect();
417    let rest: String = source
418        .lines()
419        .skip(pragmas.lines().count())
420        .map(|l| format!("{l}\n"))
421        .collect();
422    BodyFragment::PolydatSource(format!("{pragmas}{declarations}{rest}"))
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use polydat::kernel::subcontext::BodyFragment;
429
430    fn source(body: BodyFragment) -> String {
431        match body {
432            BodyFragment::PolydatSource(s) => s,
433            _ => panic!("expected source"),
434        }
435    }
436
437    #[test]
438    fn strict_declares_the_parents_coordinates_after_any_pragmas() {
439        let parent = ScopeKernel::compile("input cycle: u64\nx := 1\n").expect("parent");
440        let body = BodyFragment::PolydatSource("pragma strict\ny := cycle\n".into());
441        assert_eq!(
442            source(declare_chain_coordinates(parent.kernel(), body)),
443            "pragma strict\ninput cycle: u64\ny := cycle\n"
444        );
445    }
446
447    #[test]
448    fn a_body_that_declares_its_inputs_is_left_alone() {
449        let parent = ScopeKernel::compile("input cycle: u64\nx := 1\n").expect("parent");
450        let own = "input n: u64\ny := n\n";
451        let body = BodyFragment::PolydatSource(own.into());
452        assert_eq!(
453            source(declare_chain_coordinates(parent.kernel(), body)),
454            own
455        );
456    }
457}