Skip to main content

sim_expr_tree_calc/calc/
session.rs

1use super::attempt::{intersect_limits, parse_absolute_path};
2use super::*;
3
4impl ExprTreeCalc {
5    /// Replaces the tree-level codec policy patch.
6    pub fn set_tree_codec_policy(&mut self, patch: CodecPolicyPatch) {
7        let mut state = self.state.write().expect("calc state poisoned");
8        state.tree_codec_policy = patch;
9        bump_generation(&mut state.control_generation);
10    }
11
12    /// Replaces one directory-level codec policy patch.
13    pub fn set_dir_codec_policy(&mut self, directory: TablePath, patch: CodecPolicyPatch) {
14        let key = path_key(&directory);
15        let mut state = self.state.write().expect("calc state poisoned");
16        state.dir_codec_policies.insert(key, patch);
17        bump_generation(&mut state.control_generation);
18    }
19
20    /// Replaces one cell-level codec policy patch.
21    pub fn set_cell_codec_policy(&mut self, cell: TablePath, patch: CodecPolicyPatch) {
22        let key = path_key(&cell);
23        let mut state = self.state.write().expect("calc state poisoned");
24        state.cell_codec_policies.insert(key, patch);
25        bump_generation(&mut state.control_generation);
26    }
27
28    /// Resolves codec policy field by field from tree through ancestor
29    /// directories to the selected cell.
30    #[must_use]
31    pub fn effective_codec_policy(&self, cell: &TablePath) -> EffectiveCodecPolicy {
32        let state = self.state.read().expect("calc state poisoned");
33        effective_codec_policy(
34            &state.tree_codec_policy,
35            &state.dir_codec_policies,
36            &state.cell_codec_policies,
37            &path_key(cell),
38        )
39    }
40
41    /// Replaces the tree-level calculation policy patch.
42    pub fn set_tree_calc_policy(&mut self, patch: CalcPolicyPatch) {
43        {
44            let mut state = self.state.write().expect("calc state poisoned");
45            state.tree_calc_policy = patch;
46            bump_generation(&mut state.control_generation);
47        }
48        self.invalidate_calc_policy_matching(|_| true);
49        self.schedule_dirty_automatic();
50    }
51
52    /// Replaces one directory-level calculation policy patch.
53    pub fn set_dir_calc_policy(&mut self, directory: TablePath, patch: CalcPolicyPatch) {
54        let key = path_key(&directory);
55        {
56            let mut state = self.state.write().expect("calc state poisoned");
57            state.dir_calc_policies.insert(key, patch);
58            bump_generation(&mut state.control_generation);
59        }
60        self.invalidate_calc_policy_matching(|cell| is_descendant_or_same(&directory, cell));
61        self.schedule_dirty_automatic();
62    }
63
64    /// Replaces one cell-level calculation policy patch.
65    pub fn set_cell_calc_policy(&mut self, cell: TablePath, patch: CalcPolicyPatch) {
66        let key = path_key(&cell);
67        {
68            let mut state = self.state.write().expect("calc state poisoned");
69            state.cell_calc_policies.insert(key.clone(), patch);
70            bump_generation(&mut state.control_generation);
71        }
72        self.engine
73            .invalidate(&CalcQuery::EffectivePolicy(key.clone()));
74        self.emit_change("calculation-policy", &key);
75        self.schedule_dirty_automatic();
76    }
77
78    /// Resolves tree, ancestor-directory, and cell calculation policy fields.
79    #[must_use]
80    pub fn effective_calc_policy(&self, cell: &TablePath) -> EffectiveCalcPolicy {
81        let state = self.state.read().expect("calc state poisoned");
82        effective_calc_policy(
83            &state.tree_calc_policy,
84            &state.dir_calc_policies,
85            &state.cell_calc_policies,
86            &path_key(cell),
87        )
88    }
89
90    /// Replaces the tree-level authority policy patch.
91    pub fn set_tree_authority_policy(&mut self, patch: AuthorityPolicyPatch) {
92        {
93            let mut state = self.state.write().expect("calc state poisoned");
94            state.tree_authority_policy = patch;
95            bump_generation(&mut state.control_generation);
96        }
97        self.invalidate_authority_policy_matching(|_| true);
98        self.schedule_dirty_automatic();
99    }
100
101    /// Replaces one directory-level authority policy patch.
102    pub fn set_dir_authority_policy(&mut self, directory: TablePath, patch: AuthorityPolicyPatch) {
103        let key = path_key(&directory);
104        {
105            let mut state = self.state.write().expect("calc state poisoned");
106            state.dir_authority_policies.insert(key, patch);
107            bump_generation(&mut state.control_generation);
108        }
109        self.invalidate_authority_policy_matching(|cell| is_descendant_or_same(&directory, cell));
110        self.schedule_dirty_automatic();
111    }
112
113    /// Replaces one cell-level authority policy patch.
114    pub fn set_cell_authority_policy(&mut self, cell: TablePath, patch: AuthorityPolicyPatch) {
115        let key = path_key(&cell);
116        {
117            let mut state = self.state.write().expect("calc state poisoned");
118            state.cell_authority_policies.insert(key.clone(), patch);
119            bump_generation(&mut state.control_generation);
120        }
121        self.engine
122            .invalidate(&CalcQuery::AuthorityPolicy(key.clone()));
123        self.emit_change("authority-policy", &key);
124        self.schedule_dirty_automatic();
125    }
126
127    /// Resolves the immutable ceiling through every allow/deny policy level.
128    #[must_use]
129    pub fn effective_authority(&self, cell: &TablePath) -> EffectiveAuthority {
130        let state = self.state.read().expect("calc state poisoned");
131        effective_authority(
132            &state.authority_ceiling,
133            &state.tree_authority_policy,
134            &state.dir_authority_policies,
135            &state.cell_authority_policies,
136            &path_key(cell),
137        )
138    }
139
140    /// Updates the observed codec registry revision.
141    pub fn set_codec_registry_revision(&mut self, revision: u64) {
142        {
143            let mut state = self.state.write().expect("calc state poisoned");
144            state.codec_registry_revision = revision;
145            bump_generation(&mut state.control_generation);
146        }
147        self.engine.invalidate(&CalcQuery::CodecRegistry);
148    }
149
150    /// Adds or replaces a mounted backend observation.
151    pub fn mount(
152        &mut self,
153        path: TablePath,
154        resource: MountResource,
155        backend: BackendKind,
156        epoch: MountEpoch,
157    ) {
158        let key = path_key(&path);
159        {
160            let mut state = self.state.write().expect("calc state poisoned");
161            state.mounts.insert(
162                key.clone(),
163                MountState {
164                    resource,
165                    backend,
166                    epoch,
167                },
168            );
169            bump_generation(&mut state.control_generation);
170        }
171        self.refresh_samples
172            .insert(key.clone(), BackendRefreshSample::new(epoch));
173        self.engine.invalidate(&CalcQuery::MountEpoch(key));
174    }
175
176    /// Removes a mounted backend observation and its refresh state.
177    pub fn unmount(&mut self, path: &TablePath) -> bool {
178        let key = path_key(path);
179        let removed = {
180            let mut state = self.state.write().expect("calc state poisoned");
181            let removed = state.mounts.remove(&key).is_some();
182            if removed {
183                bump_generation(&mut state.control_generation);
184            }
185            removed
186        };
187        self.refresh_sources.remove(&key);
188        self.refresh_samples.remove(&key);
189        if removed {
190            self.engine.invalidate(&CalcQuery::MountEpoch(key));
191        }
192        removed
193    }
194
195    /// Advances a mounted backend epoch.
196    pub fn observe_mount_epoch(&mut self, path: &TablePath, epoch: MountEpoch) {
197        let key = path_key(path);
198        {
199            let mut state = self.state.write().expect("calc state poisoned");
200            if let Some(mount) = state.mounts.get_mut(&key) {
201                mount.epoch = epoch;
202                bump_generation(&mut state.control_generation);
203            }
204        }
205        self.refresh_samples
206            .entry(key.clone())
207            .and_modify(|sample| sample.epoch = epoch)
208            .or_insert_with(|| BackendRefreshSample::new(epoch));
209        self.engine.invalidate(&CalcQuery::MountEpoch(key));
210    }
211
212    /// Requests cancellation of the next calculation work that actually runs.
213    pub fn request_cancellation(&self) {
214        self.cancel_requested.store(true, Ordering::Release);
215    }
216
217    /// Pull-verifies a cell under the hard default ceilings.
218    pub fn verify_cell(&mut self, path: &TablePath) -> Result<Value, CalcError> {
219        self.verify_cell_with_limits(path, CalcLimits::default())
220    }
221
222    /// Pull-verifies a cell with requested limits clamped to hard ceilings.
223    pub fn verify_cell_with_limits(
224        &mut self,
225        path: &TablePath,
226        limits: CalcLimits,
227    ) -> Result<Value, CalcError> {
228        let mut report = self.calculate_cells([path.clone()], CalcRequestMode::Verify, limits);
229        report
230            .cells
231            .pop()
232            .map(|cell| cell.result)
233            .unwrap_or_else(|| {
234                Err(CalcError::NotCalculated {
235                    path: path_key(path),
236                })
237            })
238    }
239
240    /// Forces only this root while permitting valid dependency reuse.
241    pub fn recalculate_cell(&mut self, path: &TablePath) -> Result<Value, CalcError> {
242        self.recalculate_cell_with_limits(path, CalcLimits::default())
243    }
244
245    /// Forces only this root under explicit hard-clamped limits.
246    pub fn recalculate_cell_with_limits(
247        &mut self,
248        path: &TablePath,
249        limits: CalcLimits,
250    ) -> Result<Value, CalcError> {
251        let mut report = self.calculate_cells([path.clone()], CalcRequestMode::ForceRoots, limits);
252        report
253            .cells
254            .pop()
255            .map(|cell| cell.result)
256            .unwrap_or_else(|| {
257                Err(CalcError::NotCalculated {
258                    path: path_key(path),
259                })
260            })
261    }
262
263    /// Forces this root and every reachable calculated dependency.
264    pub fn recalculate_recursive(&mut self, path: &TablePath) -> Result<Value, CalcError> {
265        let mut report = self.calculate_cells(
266            [path.clone()],
267            CalcRequestMode::ForceRecursive,
268            CalcLimits::default(),
269        );
270        report
271            .cells
272            .pop()
273            .map(|cell| cell.result)
274            .unwrap_or_else(|| {
275                Err(CalcError::NotCalculated {
276                    path: path_key(path),
277                })
278            })
279    }
280
281    /// Runs one stable multi-root directed request through the shared engine.
282    pub fn calculate_cells(
283        &mut self,
284        roots: impl IntoIterator<Item = TablePath>,
285        mode: CalcRequestMode,
286        limits: CalcLimits,
287    ) -> DirectedCalcReport {
288        let roots = roots
289            .into_iter()
290            .map(|path| path_key(&path))
291            .collect::<BTreeSet<_>>();
292        let request_id = self.allocate_request_id();
293        let directed_cells = match mode {
294            CalcRequestMode::ForceRecursive => self.force_recursive_closure(&roots),
295            CalcRequestMode::Verify | CalcRequestMode::ForceRoots => roots.clone(),
296        };
297        match mode {
298            CalcRequestMode::Verify => {}
299            CalcRequestMode::ForceRoots => {
300                for root in &roots {
301                    self.engine.invalidate(&CalcQuery::ForceEpoch(root.clone()));
302                }
303            }
304            CalcRequestMode::ForceRecursive => {
305                for cell in &directed_cells {
306                    self.engine.invalidate(&CalcQuery::ForceEpoch(cell.clone()));
307                }
308            }
309        }
310
311        let request = ActiveRequest {
312            id: request_id,
313            reason: CalcReason::for_mode(mode),
314            directed_cells,
315            automatic: false,
316        };
317        let mut cells = Vec::new();
318        for root in roots {
319            let effective = self.effective_calc_policy(&parse_absolute_path(&root));
320            let root_limits = intersect_limits(limits, effective.budget);
321            let result = self.execute_root(root.clone(), request.clone(), root_limits, None);
322            let failed = result.is_err();
323            cells.push(DirectedCellResult { cell: root, result });
324            if failed && effective.error_mode == ErrorMode::FailFast {
325                break;
326            }
327        }
328        DirectedCalcReport { request_id, cells }
329    }
330
331    /// Opens one standard bounded stream endpoint for progress and changes.
332    pub fn watch(&mut self, buffer_policy: BufferPolicy) -> CalcWatch {
333        let watch = CalcWatch::new(self.next_watch_id, buffer_policy);
334        self.next_watch_id = self.next_watch_id.saturating_add(1);
335        self.watches.push(watch.clone());
336        watch
337    }
338
339    /// Cancels queued automatic work with this request id.
340    pub fn cancel_request(&mut self, request_id: RequestId) -> bool {
341        let key = self
342            .automatic_queue
343            .iter()
344            .find_map(|(cell, queued)| (queued.request_id == request_id).then(|| cell.clone()));
345        let Some(key) = key else {
346            return false;
347        };
348        self.automatic_queue.remove(&key);
349        self.bump_queue_generation();
350        self.emit_progress("cancelled", &key, request_id);
351        true
352    }
353
354    /// Reads only the current committed result.
355    ///
356    /// A failed or cancelled recalculation never falls back to last-good.
357    pub fn current_cell(&self, path: &TablePath) -> Result<Value, CalcError> {
358        let key = path_key(path);
359        self.state
360            .read()
361            .expect("calc state poisoned")
362            .current
363            .get(&key)
364            .cloned()
365            .unwrap_or(Err(CalcError::NotCalculated { path: key }))
366    }
367
368    /// Returns the retained historical success, explicitly labelled
369    /// `last-good`.
370    #[must_use]
371    pub fn last_good_cell(&self, path: &TablePath) -> Option<LastGoodValue> {
372        self.state
373            .read()
374            .expect("calc state poisoned")
375            .last_good
376            .get(&path_key(path))
377            .cloned()
378            .map(|value| LastGoodValue { value })
379    }
380
381    /// Returns whether the current successful result is noncanonical and must
382    /// therefore be treated as volatile.
383    #[must_use]
384    pub fn current_is_volatile(&self, path: &TablePath) -> bool {
385        self.state
386            .read()
387            .expect("calc state poisoned")
388            .volatile
389            .contains(&path_key(path))
390    }
391
392    /// Returns the current incremental memo revision.
393    #[must_use]
394    pub fn cell_revision(&self, path: &TablePath) -> Option<u64> {
395        self.engine
396            .memo_revision(&CalcQuery::Cell(path_key(path)))
397            .map(|revision| revision.get())
398    }
399
400    /// Returns the current incremental fingerprint.
401    #[must_use]
402    pub fn cell_fingerprint(&self, path: &TablePath) -> Option<ValueFingerprint> {
403        self.engine
404            .memo_fingerprint(&CalcQuery::Cell(path_key(path)))
405    }
406
407    /// Returns the latest bounded immutable calculation receipt.
408    #[must_use]
409    pub fn receipt(&self, path: &TablePath) -> Option<CalcReceipt> {
410        self.state
411            .read()
412            .expect("calc state poisoned")
413            .receipts
414            .get(&path_key(path))
415            .cloned()
416    }
417
418    /// Explains current state without evaluating or mutating the graph.
419    #[must_use]
420    pub fn explain(&self, path: &TablePath) -> CalcExplanation {
421        let cell = path_key(path);
422        let policy = self.effective_calc_policy(path);
423        let authority = self.effective_authority(path);
424        let receipt = self.receipt(path);
425        let dirty = self
426            .engine
427            .dirty_keys()
428            .contains(&CalcQuery::Cell(cell.clone()));
429        let pending = self.automatic_queue.contains_key(&cell);
430        let has_current = self
431            .state
432            .read()
433            .expect("calc state poisoned")
434            .current
435            .contains_key(&cell);
436        let status = if policy.trigger == CalcTrigger::Frozen {
437            CalcStatus::Frozen
438        } else if pending {
439            CalcStatus::Pending
440        } else if matches!(
441            receipt.as_ref().map(|receipt| &receipt.outcome),
442            Some(CalcOutcome::Blocked { .. })
443        ) {
444            CalcStatus::Blocked
445        } else if matches!(
446            receipt.as_ref().map(|receipt| &receipt.outcome),
447            Some(CalcOutcome::Failed { .. })
448        ) {
449            CalcStatus::Failed
450        } else if dirty && has_current {
451            CalcStatus::MaybeStale
452        } else if has_current {
453            CalcStatus::Fresh
454        } else {
455            CalcStatus::NeverCalculated
456        };
457        let mut reasons = Vec::new();
458        match status {
459            CalcStatus::Frozen => reasons.push("effective trigger is frozen".to_owned()),
460            CalcStatus::Pending => reasons.push("bounded automatic work is queued".to_owned()),
461            CalcStatus::Blocked | CalcStatus::Failed => {
462                if let Some(receipt) = &receipt {
463                    match &receipt.outcome {
464                        CalcOutcome::Blocked { message } | CalcOutcome::Failed { message } => {
465                            reasons.push(message.clone());
466                        }
467                        _ => {}
468                    }
469                }
470            }
471            CalcStatus::MaybeStale => {
472                reasons.push("an observed input changed and awaits verification".to_owned());
473            }
474            CalcStatus::Fresh => {
475                reasons.push("the committed memo matches all observed revisions".to_owned());
476            }
477            CalcStatus::NeverCalculated => {
478                reasons.push("no calculation attempt has committed".to_owned());
479            }
480        }
481        CalcExplanation {
482            cell: cell.clone(),
483            status,
484            source_revision: self.engine.source_revision(&CalcQuery::Cell(cell)).get(),
485            policy_digest: policy.digest(),
486            authority_digest: authority.digest(),
487            receipt,
488            reasons,
489        }
490    }
491
492    /// Runs a bounded amount of ready automatic work.
493    pub fn run_automatic(&mut self, budget: AutomaticBudget, now_ms: u64) -> AutomaticRun {
494        self.run_automatic_inner(budget, now_ms)
495    }
496
497    /// Resumes a prior automatic queue continuation.
498    pub fn continue_automatic(
499        &mut self,
500        continuation: AutomaticContinuation,
501        budget: AutomaticBudget,
502        now_ms: u64,
503    ) -> Result<AutomaticRun, CalcError> {
504        if continuation.generation() != self.automatic_generation {
505            return Err(CalcError::UnknownAutomaticContinuation {
506                generation: continuation.generation(),
507            });
508        }
509        Ok(self.run_automatic_inner(budget, now_ms))
510    }
511
512    /// Snapshots all deterministic queue state needed for restart.
513    #[must_use]
514    pub fn automatic_queue_snapshot(&self) -> AutomaticQueueSnapshot {
515        AutomaticQueueSnapshot {
516            generation: self.automatic_generation,
517            next_sequence: self.next_queue_sequence,
518            entries: self.automatic_queue.values().cloned().collect(),
519        }
520    }
521
522    /// Restores a deterministic queue snapshot, rejecting duplicate cells.
523    pub fn restore_automatic_queue(
524        &mut self,
525        snapshot: AutomaticQueueSnapshot,
526    ) -> Result<(), CalcError> {
527        let mut restored = BTreeMap::new();
528        let mut next_request_id = self.next_request_id;
529        for entry in snapshot.entries {
530            if !entry.cell.starts_with('/') {
531                return Err(CalcError::CorruptAutomaticQueue { cell: entry.cell });
532            }
533            next_request_id = next_request_id.max(entry.request_id.get().saturating_add(1));
534            let cell = entry.cell.clone();
535            if restored.insert(cell.clone(), entry).is_some() {
536                return Err(CalcError::CorruptAutomaticQueue { cell });
537            }
538        }
539        self.automatic_queue = restored;
540        self.automatic_generation = snapshot.generation.max(1);
541        self.next_queue_sequence = snapshot.next_sequence.max(1);
542        self.next_request_id = next_request_id;
543        Ok(())
544    }
545}