Skip to main content

polydat_core/kernel/
scope.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Scope coordinates — the formal Polydat-side model of the
5//! iteration position a kernel occupies inside an enclosing
6//! comprehension chain.
7//!
8//! ## Definition
9//!
10//! A **scope coordinate set** for a single scope is the
11//! ordered name→value tuple of every iteration extern that
12//! scope owns — i.e. variables the scope declared via
13//! `extern <var>: <type>` and that aren't mirroring an outer
14//! scope (`is_inherited` returns false in this scope's program).
15//! The order is the declaration order from the comprehension's
16//! source (preserved by `IndexMap`'s insertion semantics).
17//!
18//! A **scope coordinate path** is the leaf-first list of
19//! coordinate sets, walking from the kernel's own scope up
20//! through every enclosing comprehension scope. Workload-root
21//! params (top-level `params:` in the document) don't
22//! contribute — they're configuration, not iteration
23//! coordinates.
24//!
25//! ## Invariant
26//!
27//! Every kernel that has been *initialised in its scope* —
28//! either via `PolydatKernel::build_subscope` (post-bind
29//! the path is `[own] ++ outer.scope_coordinates()`), or as a
30//! root scope (path is `[own]` if non-empty, else empty) —
31//! has [`super::PolydatKernel::scope_coordinates`] populated. This
32//! is treated as a structural invariant of the Polydat model, not
33//! an optional add-on: the runtime contract is that any
34//! consumer (presentation layer, inspector, future scope-aware
35//! diagnostics) can call `scope_coordinates()` on an
36//! initialised kernel and get the full path back without
37//! needing to walk the scope tree itself.
38//!
39//! ## Use
40//!
41//! Presentation-layer consumers (the inline status line, TUI
42//! phase rows, the inspector socket) render the path as
43//! striated parens — `(leaf coords), (parent coords), …` —
44//! so the operator can read the active iteration off each
45//! enclosing scope at a glance. Without striation the
46//! operator can't tell which `k=10` belongs to the inner
47//! comprehension vs. an outer one with the same coord name
48//! in a different shape.
49//!
50//! See SRD 18b §"Iteration variables as scope outputs" for
51//! how comprehension scopes synthesise the `extern` slots
52//! that this module classifies as coordinates.
53
54use indexmap::IndexMap;
55
56use crate::ast::Value;
57
58/// One scope's worth of iteration coordinates — the LHS names
59/// and current values of every `extern <var>: <type>` clause
60/// that scope declared (excluding ones inherited from a parent).
61///
62/// Ordered by declaration position. The map is empty for scopes
63/// that don't own any coordinates (e.g. a scenario node that's
64/// just a list of phases — no comprehension at that level).
65#[derive(Clone, Debug, Default)]
66pub struct ScopeCoord {
67    /// The coordinates, in declaration order.
68    pub vars: IndexMap<String, Value>,
69}
70
71impl ScopeCoord {
72    /// No coordinates.
73    pub fn new() -> Self {
74        Self {
75            vars: IndexMap::new(),
76        }
77    }
78    /// Whether the scope owns no coordinate.
79    pub fn is_empty(&self) -> bool {
80        self.vars.is_empty()
81    }
82    /// The number of coordinates.
83    pub fn len(&self) -> usize {
84        self.vars.len()
85    }
86}
87
88/// Helper for building a coord set from `(name, Value)` pairs.
89impl<I> From<I> for ScopeCoord
90where
91    I: IntoIterator<Item = (String, Value)>,
92{
93    fn from(it: I) -> Self {
94        Self {
95            vars: it.into_iter().collect(),
96        }
97    }
98}
99
100/// Format a scope-coordinate path as striated parens, leaf-first:
101/// `(k=10, limit=20), (table=…, optimize_for=…)`. Empty strata
102/// are skipped, so a chain that passes through a non-comprehension
103/// scope (e.g. a scenario node that's just a phase list) doesn't
104/// render an empty `()`. Returns `""` for an empty path so callers
105/// can wrap with `(…)` parens at their own discretion.
106///
107/// **Canonical structural identity.** This is the formatter every
108/// consumer reasoning about a kernel's iteration position runs
109/// through — runtime executor labels, pre-map walker labels,
110/// inline status lines, scene-tree labels, error messages. Pre-map
111/// and runtime producing the same string for the same iteration
112/// position is what lets observer lifecycle calls bind to
113/// pre-mapped scene nodes without a parallel matching scheme.
114pub fn format_scope_coordinate_path(path: &[ScopeCoord]) -> String {
115    let strata: Vec<String> = path
116        .iter()
117        .filter(|c| !c.is_empty())
118        .map(|coord| {
119            let inner = coord
120                .vars
121                .iter()
122                .map(|(k, v)| format!("{k}={}", v.to_display_string()))
123                .collect::<Vec<_>>()
124                .join(", ");
125            format!("({inner})")
126        })
127        .collect();
128    strata.join(", ")
129}