Skip to main content

sim_expr_tree_calc/
calc.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::{
4        Arc, RwLock,
5        atomic::{AtomicBool, AtomicU64, Ordering},
6    },
7};
8
9use sim_expr_tree_core::{
10    BackendKind, CodecPolicyPatch, EffectiveCodecPolicy, MountEpoch, MountResource,
11};
12use sim_incremental_core::{
13    ContinuationToken, IncrementalEngine, IncrementalError, ObservationKind, SnapshotBudgets,
14    ValueFingerprint,
15};
16use sim_kernel::{
17    CapabilitySet, Cx, DefaultFactory, EagerPolicy, Expr, HandleSeed, StrictNames, Symbol, Value,
18};
19use sim_lib_stream_core::BufferPolicy;
20use sim_table_core::TablePath;
21
22use crate::ExprTreeRefPolicy;
23
24mod attempt;
25mod engine;
26mod eval;
27use eval::{evaluate_cell, observe_runtime_context, parent_path, path_key};
28mod face;
29pub use face::{
30    EncodedFace, FaceContent, FaceDimension, FaceIssue, FaceMetadata, FacePosition,
31    SourceEditOutcome,
32};
33mod model;
34pub use model::{
35    CalcError, CalcLimits, CalcQuery, CellFailure, HARD_MAX_EXPR_DEPTH, HARD_MAX_OBSERVATIONS,
36    HARD_MAX_OUTPUT, HARD_MAX_QUERY_DEPTH, HARD_MAX_WORK, LastGoodValue,
37};
38use model::{ContextFactory, MemoOutcome, MemoValue};
39mod policy;
40pub use policy::{
41    AuthorityDigest, AuthorityPolicyPatch, CalcPolicyPatch, CalcTrigger, CycleMode,
42    EffectiveAuthority, EffectiveCalcPolicy, ErrorMode, PolicyDigest,
43};
44use policy::{
45    effective_authority, effective_calc_policy, effective_codec_policy, is_descendant_or_same,
46};
47mod persistence;
48pub use persistence::{
49    DERIVED_SNAPSHOT_KEY, DerivedPersistReport, DerivedRestoreDisposition, DerivedRestoreReport,
50    DerivedSnapshotError, DerivedTableAdapter, GRAPH_SCHEMA_VERSION,
51};
52mod refresh;
53pub use refresh::{BackendRefreshSample, MountRefreshSource, RefreshError, RefreshReport};
54mod receipt;
55pub use receipt::{
56    CalcExplanation, CalcOutcome, CalcReason, CalcReceipt, CalcRequestMode, CalcStatus,
57    DependencyStamp, DirectedCalcReport, DirectedCellResult, EffectStamp, RequestId,
58};
59mod scheduler;
60use scheduler::MAX_READY_BYPASSES;
61pub use scheduler::{
62    AutomaticBudget, AutomaticContinuation, AutomaticQueueSnapshot, AutomaticRun, QueuedCalculation,
63};
64mod scheduling;
65mod session;
66mod value;
67mod watch;
68pub use watch::CalcWatch;
69
70const MAX_RECEIPT_DEPENDENCIES: usize = 64;
71const MAX_RECEIPT_GRAPH_NODES: usize = 4_096;
72const MAX_RECEIPT_GRAPH_EDGES: usize = 65_536;
73
74use sim_host_core::WallClock;
75
76/// Incremental calculator for ordinary SIM [`Expr`] sources and [`Value`]
77/// results.
78pub struct ExprTreeCalc {
79    state: Arc<RwLock<CalcState>>,
80    engine: IncrementalEngine<CalcQuery, MemoValue>,
81    context_factory: Arc<ContextFactory>,
82    cancel_requested: Arc<AtomicBool>,
83    next_volatile: Arc<AtomicU64>,
84    wall_clock: Arc<RwLock<Option<Arc<dyn WallClock>>>>,
85    next_request_id: u64,
86    automatic_queue: BTreeMap<String, QueuedCalculation>,
87    automatic_generation: u64,
88    next_queue_sequence: u64,
89    watches: Vec<CalcWatch>,
90    next_watch_id: u64,
91    refresh_sources: BTreeMap<String, Arc<dyn MountRefreshSource>>,
92    refresh_samples: BTreeMap<String, BackendRefreshSample>,
93    restored_continuations: BTreeSet<ContinuationToken>,
94}
95
96#[derive(Default)]
97pub(crate) struct CalcState {
98    cells: BTreeMap<String, Expr>,
99    bound_names: BTreeSet<String>,
100    bound_values: BTreeMap<Symbol, Value>,
101    mounts: BTreeMap<String, MountState>,
102    codec_registry_revision: u64,
103    tree_calc_policy: CalcPolicyPatch,
104    dir_calc_policies: BTreeMap<String, CalcPolicyPatch>,
105    cell_calc_policies: BTreeMap<String, CalcPolicyPatch>,
106    tree_codec_policy: CodecPolicyPatch,
107    dir_codec_policies: BTreeMap<String, CodecPolicyPatch>,
108    cell_codec_policies: BTreeMap<String, CodecPolicyPatch>,
109    authority_ceiling: CapabilitySet,
110    tree_authority_policy: AuthorityPolicyPatch,
111    dir_authority_policies: BTreeMap<String, AuthorityPolicyPatch>,
112    cell_authority_policies: BTreeMap<String, AuthorityPolicyPatch>,
113    active_request: Option<ActiveRequest>,
114    attempts: Vec<AttemptDraft>,
115    receipts: BTreeMap<String, CalcReceipt>,
116    next_logical_tick: u64,
117    current: BTreeMap<String, Result<Value, CalcError>>,
118    last_good: BTreeMap<String, Value>,
119    volatile: BTreeSet<String>,
120    failed_cells: BTreeSet<String>,
121    source_generation: u64,
122    control_generation: u64,
123}
124
125#[derive(Clone)]
126struct ActiveRequest {
127    id: RequestId,
128    reason: CalcReason,
129    directed_cells: BTreeSet<String>,
130    automatic: bool,
131}
132
133struct AttemptDraft {
134    request_id: RequestId,
135    cell: String,
136    policy: EffectiveCalcPolicy,
137    authority: EffectiveAuthority,
138    started_tick: u64,
139    finished_tick: u64,
140    wall_started_ms: Option<u64>,
141    wall_finished_ms: Option<u64>,
142    outcome: CalcOutcome,
143    effects: Vec<EffectStamp>,
144    omitted_effects: usize,
145    reason: CalcReason,
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
149struct MountState {
150    resource: MountResource,
151    backend: BackendKind,
152    epoch: MountEpoch,
153}
154
155impl ExprTreeCalc {
156    /// Creates a calculator using strict eager ordinary SIM evaluation.
157    #[must_use]
158    pub fn new(first_handle_seed: HandleSeed) -> Self {
159        let next_handle_seed = Arc::new(AtomicU64::new(first_handle_seed.0));
160        Self::with_context_factory(move || {
161            let seed = next_handle_seed
162                .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |seed| {
163                    seed.checked_add(1)
164                })
165                .expect("expression-tree handle seed space exhausted");
166            Cx::new(
167                Arc::new(ExprTreeRefPolicy::new(StrictNames(EagerPolicy))),
168                Arc::new(DefaultFactory),
169                HandleSeed::new(seed),
170            )
171        })
172    }
173
174    /// Creates a calculator from a fresh-context factory.
175    ///
176    /// The factory may install loadable libraries, lexical values, functions,
177    /// macros, tables, and directories. It is invoked with no calculator lock
178    /// held.
179    #[must_use]
180    pub fn with_context_factory<F>(factory: F) -> Self
181    where
182        F: Fn() -> Cx + Send + Sync + 'static,
183    {
184        let context_factory: Arc<ContextFactory> = Arc::new(factory);
185        let open_time_authority = context_factory().capabilities().clone();
186        Self {
187            state: Arc::new(RwLock::new(CalcState {
188                authority_ceiling: open_time_authority,
189                next_logical_tick: 1,
190                ..CalcState::default()
191            })),
192            engine: IncrementalEngine::new(),
193            context_factory,
194            cancel_requested: Arc::new(AtomicBool::new(false)),
195            next_volatile: Arc::new(AtomicU64::new(1)),
196            wall_clock: Arc::new(RwLock::new(None)),
197            next_request_id: 1,
198            automatic_queue: BTreeMap::new(),
199            automatic_generation: 1,
200            next_queue_sequence: 1,
201            watches: Vec::new(),
202            next_watch_id: 1,
203            refresh_sources: BTreeMap::new(),
204            refresh_samples: BTreeMap::new(),
205            restored_continuations: BTreeSet::new(),
206        }
207    }
208
209    /// Replaces the optional human wall-clock observation source.
210    ///
211    /// Logical ticks and revisions remain the only freshness authority.
212    pub fn set_wall_clock(&mut self, clock: Arc<dyn WallClock>) {
213        *self.wall_clock.write().expect("wall clock lock poisoned") = Some(clock);
214    }
215
216    /// Returns the immutable capability ceiling captured when this tree opened.
217    #[must_use]
218    pub fn open_time_authority(&self) -> CapabilitySet {
219        self.state
220            .read()
221            .expect("calc state poisoned")
222            .authority_ceiling
223            .clone()
224    }
225
226    /// Installs or replaces an ordinary expression source.
227    pub fn set_cell(&mut self, path: TablePath, source: Expr) {
228        let key = path_key(&path);
229        let (replaced, failed) = {
230            let mut state = self.state.write().expect("calc state poisoned");
231            let replaced = state.cells.insert(key.clone(), source).is_some();
232            bump_generation(&mut state.source_generation);
233            state.current.remove(&key);
234            state.volatile.remove(&key);
235            (
236                replaced,
237                state.failed_cells.iter().cloned().collect::<Vec<_>>(),
238            )
239        };
240        if !replaced {
241            self.register_cell_query(key.clone());
242        }
243        self.invalidate_cell_source(&path, !replaced);
244        self.invalidate_failed_cells(failed);
245        self.emit_change("source-set", &key);
246        self.schedule_dirty_automatic();
247    }
248
249    /// Removes a cell source while retaining an explicit missing-value query.
250    pub fn remove_cell(&mut self, path: &TablePath) {
251        let key = path_key(path);
252        let failed = {
253            let mut state = self.state.write().expect("calc state poisoned");
254            state.cells.remove(&key);
255            bump_generation(&mut state.source_generation);
256            state.current.remove(&key);
257            state.volatile.remove(&key);
258            state.failed_cells.iter().cloned().collect::<Vec<_>>()
259        };
260        self.register_cell_query(key.clone());
261        self.invalidate_cell_source(path, true);
262        self.invalidate_failed_cells(failed);
263        self.emit_change("source-removed", &key);
264        self.schedule_dirty_automatic();
265    }
266
267    /// Moves an ordinary source and invalidates both namespace locations.
268    pub fn move_cell(&mut self, from: &TablePath, to: TablePath) {
269        let from_key = path_key(from);
270        let to_key = path_key(&to);
271        let (moved, failed) = {
272            let mut state = self.state.write().expect("calc state poisoned");
273            let moved = state.cells.remove(&from_key);
274            if let Some(source) = moved.clone() {
275                state.cells.insert(to_key.clone(), source);
276            }
277            bump_generation(&mut state.source_generation);
278            state.current.remove(&from_key);
279            state.current.remove(&to_key);
280            state.volatile.remove(&from_key);
281            state.volatile.remove(&to_key);
282            let failed = state.failed_cells.iter().cloned().collect::<Vec<_>>();
283            (moved, failed)
284        };
285        if moved.is_some() {
286            self.register_cell_query(to_key.clone());
287        }
288        self.register_cell_query(from_key.clone());
289        self.invalidate_cell_source(from, true);
290        self.invalidate_cell_source(&to, true);
291        self.invalidate_failed_cells(failed);
292        self.emit_change("source-moved-from", &from_key);
293        self.emit_change("source-moved-to", &to_key);
294        self.schedule_dirty_automatic();
295    }
296
297    /// Binds a lexical name to a diagnostic string value.
298    ///
299    /// This compatibility helper keeps a name ahead of tree lookup. New code
300    /// should prefer [`Self::bind_value`].
301    pub fn bind_name(&mut self, name: impl Into<String>) {
302        let name = name.into();
303        {
304            let mut state = self.state.write().expect("calc state poisoned");
305            state.bound_names.insert(name.clone());
306            bump_generation(&mut state.source_generation);
307        }
308        self.engine.invalidate(&CalcQuery::NameSlot(name));
309    }
310
311    /// Binds an arbitrary ordinary SIM value ahead of tree-name lookup.
312    pub fn bind_value(&mut self, name: Symbol, value: Value) {
313        {
314            let mut state = self.state.write().expect("calc state poisoned");
315            state.bound_values.insert(name.clone(), value);
316            bump_generation(&mut state.source_generation);
317        }
318        self.engine
319            .invalidate(&CalcQuery::NameSlot(name.to_string()));
320    }
321
322    /// Returns the dependency observations for a cell in deterministic order.
323    pub fn cell_dependencies(
324        &mut self,
325        path: &TablePath,
326    ) -> Result<Vec<(CalcQuery, ObservationKind)>, IncrementalError<CalcQuery>> {
327        let key = CalcQuery::Cell(path_key(path));
328        let snapshot = self
329            .engine
330            .snapshot([key.clone()], SnapshotBudgets::default())?;
331        Ok(snapshot
332            .nodes
333            .iter()
334            .find(|node| node.key == key)
335            .map(|node| {
336                node.dependencies
337                    .iter()
338                    .map(|observation| (observation.key().clone(), observation.kind().clone()))
339                    .collect()
340            })
341            .unwrap_or_default())
342    }
343
344    #[cfg(test)]
345    pub(crate) fn replace_context_factory<F>(&mut self, factory: F)
346    where
347        F: Fn() -> Cx + Send + Sync + 'static,
348    {
349        self.context_factory = Arc::new(factory);
350    }
351
352    #[cfg(test)]
353    pub(crate) fn state_for_lock_probe(&self) -> Arc<RwLock<CalcState>> {
354        Arc::clone(&self.state)
355    }
356}
357
358fn incremental_failure(
359    error: IncrementalError<CalcQuery>,
360) -> Result<MemoValue, IncrementalError<CalcQuery>> {
361    match error {
362        IncrementalError::Cycle { path } => Ok(MemoValue::failure(CellFailure::Cycle { path })),
363        IncrementalError::UnknownQuery { key } => Ok(MemoValue::failure(CellFailure::Evaluation {
364            message: format!("unknown dependency {key:?}"),
365        })),
366        IncrementalError::BudgetExceeded { .. }
367        | IncrementalError::Cancelled
368        | IncrementalError::UnknownContinuation { .. } => Err(error),
369    }
370}
371
372pub(super) fn bump_generation(generation: &mut u64) {
373    *generation = generation.saturating_add(1);
374}