Skip to main content

sim_expr_tree_calc/calc/
policy.rs

1use sim_expr_tree_core::{CodecPolicyPatch, EffectiveCodecPolicy};
2use sim_kernel::{CapabilityName, CapabilitySet};
3use sim_table_core::TablePath;
4
5use super::CalcLimits;
6
7/// Mutation behavior for a calculated cell.
8#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
9pub enum CalcTrigger {
10    /// Mutations enqueue bounded calculation work.
11    #[default]
12    Automatic,
13    /// Work begins only when a result is requested.
14    OnDemand,
15    /// Work begins only when this cell is an explicitly directed root.
16    Manual,
17    /// The committed memo is retained and no new work is permitted.
18    Frozen,
19}
20
21/// How a multi-root request responds to one failed root.
22#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
23pub enum ErrorMode {
24    /// Continue to the remaining roots and report every outcome.
25    #[default]
26    Continue,
27    /// Stop after the first failed or blocked root.
28    FailFast,
29}
30
31/// How a dynamic dependency cycle is represented.
32#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
33pub enum CycleMode {
34    /// Commit the deterministic cycle as a calculation failure.
35    #[default]
36    Fail,
37    /// Commit the deterministic cycle as a blocked calculation.
38    Block,
39}
40
41/// Field-by-field calculation policy override at one tree, directory, or cell.
42#[derive(Clone, Debug, Default, Eq, PartialEq)]
43pub struct CalcPolicyPatch {
44    /// Optional trigger override.
45    pub trigger: Option<CalcTrigger>,
46    /// Optional multi-root error behavior override.
47    pub error_mode: Option<ErrorMode>,
48    /// Optional cycle behavior override.
49    pub cycle_mode: Option<CycleMode>,
50    /// Optional incremental-query budget override.
51    pub budget: Option<CalcLimits>,
52    /// Optional scheduler priority override.
53    pub priority: Option<i16>,
54    /// Optional automatic-work debounce override.
55    pub debounce_ms: Option<u32>,
56}
57
58impl CalcPolicyPatch {
59    /// Applies this patch to an already-effective parent policy.
60    pub fn apply_to(&self, effective: &mut EffectiveCalcPolicy) {
61        if let Some(trigger) = self.trigger {
62            effective.trigger = trigger;
63        }
64        if let Some(error_mode) = self.error_mode {
65            effective.error_mode = error_mode;
66        }
67        if let Some(cycle_mode) = self.cycle_mode {
68            effective.cycle_mode = cycle_mode;
69        }
70        if let Some(budget) = self.budget {
71            effective.budget = budget;
72        }
73        if let Some(priority) = self.priority {
74            effective.priority = priority;
75        }
76        if let Some(debounce_ms) = self.debounce_ms {
77            effective.debounce_ms = debounce_ms;
78        }
79    }
80}
81
82/// Fully inherited calculation policy for one cell.
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub struct EffectiveCalcPolicy {
85    /// Effective trigger mode.
86    pub trigger: CalcTrigger,
87    /// Effective multi-root error behavior.
88    pub error_mode: ErrorMode,
89    /// Effective cycle behavior.
90    pub cycle_mode: CycleMode,
91    /// Effective request budget before hard host clamping.
92    pub budget: CalcLimits,
93    /// Effective scheduler priority. Larger values run first.
94    pub priority: i16,
95    /// Effective automatic-work debounce in milliseconds.
96    pub debounce_ms: u32,
97}
98
99impl Default for EffectiveCalcPolicy {
100    fn default() -> Self {
101        Self {
102            trigger: CalcTrigger::Automatic,
103            error_mode: ErrorMode::Continue,
104            cycle_mode: CycleMode::Fail,
105            budget: CalcLimits::default(),
106            priority: 0,
107            debounce_ms: 0,
108        }
109    }
110}
111
112impl EffectiveCalcPolicy {
113    /// Returns a deterministic digest of every effective field.
114    #[must_use]
115    pub fn digest(self) -> PolicyDigest {
116        let mut digest = StableDigest::new();
117        digest.write(match self.trigger {
118            CalcTrigger::Automatic => b"automatic",
119            CalcTrigger::OnDemand => b"on-demand",
120            CalcTrigger::Manual => b"manual",
121            CalcTrigger::Frozen => b"frozen",
122        });
123        digest.write(match self.error_mode {
124            ErrorMode::Continue => b"continue",
125            ErrorMode::FailFast => b"fail-fast",
126        });
127        digest.write(match self.cycle_mode {
128            CycleMode::Fail => b"cycle-fail",
129            CycleMode::Block => b"cycle-block",
130        });
131        digest.write(&self.budget.max_work.to_le_bytes());
132        digest.write(&self.budget.max_observations.to_le_bytes());
133        digest.write(&self.budget.max_query_depth.to_le_bytes());
134        digest.write(&self.budget.max_output.to_le_bytes());
135        digest.write(&self.priority.to_le_bytes());
136        digest.write(&self.debounce_ms.to_le_bytes());
137        PolicyDigest(digest.finish())
138    }
139}
140
141/// Capability allow/deny/require override at one policy level.
142///
143/// `allow = None` inherits the current authority. `allow = Some(empty)` denies
144/// every capability. Denials accumulate and can never be re-granted by a
145/// descendant patch.
146#[derive(Clone, Debug, Default, Eq, PartialEq)]
147pub struct AuthorityPolicyPatch {
148    /// Optional allow-list intersection at this level.
149    pub allow: Option<CapabilitySet>,
150    /// Capabilities removed at this level and below.
151    pub deny: CapabilitySet,
152    /// Capabilities that must remain after diminution before evaluation.
153    pub required: CapabilitySet,
154}
155
156/// Fully diminished authority and requirements for one cell.
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub struct EffectiveAuthority {
159    capabilities: CapabilitySet,
160    required: CapabilitySet,
161    denied: CapabilitySet,
162    digest: AuthorityDigest,
163}
164
165impl EffectiveAuthority {
166    /// Derives cell authority from the immutable open-time ceiling.
167    #[must_use]
168    pub fn derive(
169        ceiling: &CapabilitySet,
170        patches: impl IntoIterator<Item = AuthorityPolicyPatch>,
171    ) -> Self {
172        let mut capabilities = ceiling.clone();
173        let mut required = CapabilitySet::new();
174        let mut denied = CapabilitySet::new();
175        for patch in patches {
176            if let Some(allow) = patch.allow {
177                capabilities = capabilities.intersect(&allow);
178            }
179            for capability in patch.deny.iter().cloned() {
180                denied.insert(capability);
181            }
182            for capability in patch.required.iter().cloned() {
183                required.insert(capability);
184            }
185            capabilities = without_denied(&capabilities, &denied);
186        }
187        let digest = authority_digest(&capabilities);
188        Self {
189            capabilities,
190            required,
191            denied,
192            digest,
193        }
194    }
195
196    /// Returns the active diminished capability set.
197    #[must_use]
198    pub fn capabilities(&self) -> &CapabilitySet {
199        &self.capabilities
200    }
201
202    /// Returns the accumulated required capability set.
203    #[must_use]
204    pub fn required(&self) -> &CapabilitySet {
205        &self.required
206    }
207
208    /// Returns the accumulated denial set.
209    #[must_use]
210    pub fn denied(&self) -> &CapabilitySet {
211        &self.denied
212    }
213
214    /// Returns the deterministic digest of the active authority.
215    #[must_use]
216    pub const fn digest(&self) -> AuthorityDigest {
217        self.digest
218    }
219
220    /// Returns the first missing requirement in stable name order.
221    #[must_use]
222    pub fn first_missing_requirement(&self) -> Option<CapabilityName> {
223        self.required
224            .iter()
225            .find(|capability| !self.capabilities.contains(capability))
226            .cloned()
227    }
228}
229
230/// Compact deterministic calculation-policy digest.
231#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
232pub struct PolicyDigest(u64);
233
234impl PolicyDigest {
235    pub(super) const fn from_persisted(value: u64) -> Self {
236        Self(value)
237    }
238
239    /// Returns the digest bits.
240    #[must_use]
241    pub const fn get(self) -> u64 {
242        self.0
243    }
244}
245
246/// Compact deterministic effective-authority digest.
247#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
248pub struct AuthorityDigest(u64);
249
250impl AuthorityDigest {
251    pub(super) const fn from_persisted(value: u64) -> Self {
252        Self(value)
253    }
254
255    /// Returns the digest bits.
256    #[must_use]
257    pub const fn get(self) -> u64 {
258        self.0
259    }
260}
261
262pub(super) fn effective_calc_policy(
263    tree: &CalcPolicyPatch,
264    directories: &std::collections::BTreeMap<String, CalcPolicyPatch>,
265    cells: &std::collections::BTreeMap<String, CalcPolicyPatch>,
266    cell: &str,
267) -> EffectiveCalcPolicy {
268    let mut effective = EffectiveCalcPolicy::default();
269    tree.apply_to(&mut effective);
270    for ancestor in ancestor_directories(cell) {
271        if let Some(patch) = directories.get(&ancestor) {
272            patch.apply_to(&mut effective);
273        }
274    }
275    if let Some(patch) = cells.get(cell) {
276        patch.apply_to(&mut effective);
277    }
278    effective
279}
280
281pub(super) fn effective_authority(
282    ceiling: &CapabilitySet,
283    tree: &AuthorityPolicyPatch,
284    directories: &std::collections::BTreeMap<String, AuthorityPolicyPatch>,
285    cells: &std::collections::BTreeMap<String, AuthorityPolicyPatch>,
286    cell: &str,
287) -> EffectiveAuthority {
288    let mut patches = vec![tree.clone()];
289    patches.extend(
290        ancestor_directories(cell)
291            .into_iter()
292            .filter_map(|ancestor| directories.get(&ancestor).cloned()),
293    );
294    if let Some(patch) = cells.get(cell) {
295        patches.push(patch.clone());
296    }
297    EffectiveAuthority::derive(ceiling, patches)
298}
299
300pub(super) fn effective_codec_policy(
301    tree: &CodecPolicyPatch,
302    directories: &std::collections::BTreeMap<String, CodecPolicyPatch>,
303    cells: &std::collections::BTreeMap<String, CodecPolicyPatch>,
304    cell: &str,
305) -> EffectiveCodecPolicy {
306    let mut patches = vec![tree.clone()];
307    patches.extend(
308        ancestor_directories(cell)
309            .into_iter()
310            .filter_map(|ancestor| directories.get(&ancestor).cloned()),
311    );
312    if let Some(patch) = cells.get(cell) {
313        patches.push(patch.clone());
314    }
315    EffectiveCodecPolicy::derive(patches)
316}
317
318pub(super) fn is_descendant_or_same(directory: &TablePath, cell: &TablePath) -> bool {
319    let directory_segments = directory.segments();
320    let cell_segments = cell.segments();
321    directory_segments.len() <= cell_segments.len()
322        && directory_segments
323            .iter()
324            .zip(cell_segments)
325            .all(|(left, right)| left == right)
326}
327
328fn ancestor_directories(cell: &str) -> Vec<String> {
329    let mut ancestors = vec!["/".to_owned()];
330    let mut segments = cell
331        .trim_start_matches('/')
332        .split('/')
333        .filter(|segment| !segment.is_empty())
334        .collect::<Vec<_>>();
335    segments.pop();
336    let mut current = String::new();
337    for segment in segments {
338        current.push('/');
339        current.push_str(segment);
340        ancestors.push(current.clone());
341    }
342    ancestors
343}
344
345fn without_denied(capabilities: &CapabilitySet, denied: &CapabilitySet) -> CapabilitySet {
346    capabilities
347        .iter()
348        .filter(|capability| !denied.contains(capability))
349        .cloned()
350        .fold(CapabilitySet::new(), CapabilitySet::grant)
351}
352
353fn authority_digest(capabilities: &CapabilitySet) -> AuthorityDigest {
354    let mut digest = StableDigest::new();
355    for capability in capabilities.iter() {
356        digest.write(capability.as_str().as_bytes());
357        digest.write(&[0]);
358    }
359    AuthorityDigest(digest.finish())
360}
361
362struct StableDigest(u64);
363
364impl StableDigest {
365    const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
366    const PRIME: u64 = 0x0000_0100_0000_01b3;
367
368    fn new() -> Self {
369        Self(Self::OFFSET)
370    }
371
372    fn write(&mut self, bytes: &[u8]) {
373        for byte in bytes {
374            self.0 ^= u64::from(*byte);
375            self.0 = self.0.wrapping_mul(Self::PRIME);
376        }
377    }
378
379    fn finish(self) -> u64 {
380        self.0
381    }
382}