Skip to main content

bicmath_core/
context.rs

1//! Execution and effective numeric contexts.
2
3use std::time::Instant;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::{EngineError, ErrorCode};
8use crate::limits::{CancellationToken, Limits};
9pub use crate::number::NumericContext;
10
11/// Working-precision preset requested by the caller.
12///
13/// A budget selects only the working precision. It deliberately does **not**
14/// change method defaults, tolerances, or convergence criteria: different
15/// algorithms have different conditioning, and a tighter requested tolerance
16/// does not by itself produce a more accurate answer. Iterative methods report
17/// their own requested tolerance, error estimate, residual, and convergence
18/// status as separate fields, and decimal calculations that round still report
19/// `rounded`.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Budget {
23    /// 15 significant digits.
24    Fast,
25    /// 34 significant digits (default).
26    Balanced,
27    /// 100 significant digits.
28    Precise,
29}
30
31impl Budget {
32    /// Working precision in significant digits.
33    pub fn precision(self) -> u32 {
34        match self {
35            Budget::Fast => 15,
36            Budget::Balanced => 34,
37            Budget::Precise => 100,
38        }
39    }
40
41    pub fn as_str(self) -> &'static str {
42        match self {
43            Budget::Fast => "fast",
44            Budget::Balanced => "balanced",
45            Budget::Precise => "precise",
46        }
47    }
48}
49
50/// Trace verbosity requested by the caller.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum TraceLevel {
54    /// No trace is recorded.
55    #[default]
56    Off,
57    /// Only top-level steps are recorded.
58    Summary,
59    /// All bounded intermediate steps are recorded, subject to `max_trace_bytes`.
60    Full,
61}
62
63/// A recorded numeric conversion or promotion, for provenance.
64#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct Conversion {
66    pub from: String,
67    pub to: String,
68    pub reason: String,
69    pub exact: bool,
70}
71
72/// The subset of applied limits that can affect an algorithm or its result.
73#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
74pub struct AppliedLimits {
75    pub request_timeout_ms: u64,
76    pub max_operations: u64,
77    pub max_iterations: u64,
78    pub max_array_len: usize,
79    pub max_matrix_elements: usize,
80    pub max_batch_nodes: usize,
81    pub max_output_bytes: usize,
82}
83
84impl From<&Limits> for AppliedLimits {
85    fn from(limits: &Limits) -> Self {
86        AppliedLimits {
87            request_timeout_ms: limits.request_timeout_ms,
88            max_operations: limits.max_operations,
89            max_iterations: limits.max_iterations,
90            max_array_len: limits.max_array_len,
91            max_matrix_elements: limits.max_matrix_elements,
92            max_batch_nodes: limits.max_batch_nodes,
93            max_output_bytes: limits.max_output_bytes,
94        }
95    }
96}
97
98/// The effective context recorded in a result envelope.
99#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100pub struct EffectiveContext {
101    pub numeric: NumericContext,
102    /// The requested budget preset, when one was supplied. The numeric
103    /// context records the effective precision; methods report their own
104    /// achieved tolerances and convergence.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub budget: Option<Budget>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub seed: Option<u64>,
109    pub limits: AppliedLimits,
110    #[serde(default, skip_serializing_if = "Vec::is_empty")]
111    pub conversions: Vec<Conversion>,
112}
113
114/// The context passed to every function invocation.
115#[derive(Clone, Debug)]
116pub struct ExecContext {
117    pub numeric: NumericContext,
118    /// Budget preset, used by iterative methods for default tolerances.
119    pub budget: Option<Budget>,
120    pub limits: Limits,
121    pub cancellation: CancellationToken,
122    pub deadline: Option<Instant>,
123    pub seed: Option<u64>,
124    pub trace: TraceLevel,
125}
126
127impl Default for ExecContext {
128    fn default() -> Self {
129        ExecContext::conservative()
130    }
131}
132
133impl ExecContext {
134    pub fn conservative() -> ExecContext {
135        let limits = Limits::conservative();
136        ExecContext {
137            deadline: limits.deadline(),
138            numeric: NumericContext::default(),
139            budget: None,
140            limits,
141            cancellation: CancellationToken::new(),
142            seed: None,
143            trace: TraceLevel::Off,
144        }
145    }
146
147    pub fn with_numeric(mut self, numeric: NumericContext) -> ExecContext {
148        self.numeric = numeric;
149        self
150    }
151
152    pub fn with_limits(mut self, limits: Limits) -> ExecContext {
153        self.deadline = limits.deadline();
154        self.limits = limits;
155        self
156    }
157
158    pub fn with_seed(mut self, seed: Option<u64>) -> ExecContext {
159        self.seed = seed;
160        self
161    }
162
163    pub fn with_trace(mut self, trace: TraceLevel) -> ExecContext {
164        self.trace = trace;
165        self
166    }
167
168    pub fn with_budget(mut self, budget: Option<Budget>) -> ExecContext {
169        self.budget = budget;
170        if let Some(budget) = budget {
171            self.numeric.precision = budget.precision();
172        }
173        self
174    }
175
176    pub fn exact() -> ExecContext {
177        ExecContext {
178            numeric: NumericContext::exact(),
179            ..ExecContext::conservative()
180        }
181    }
182
183    pub fn scientific() -> ExecContext {
184        ExecContext {
185            numeric: NumericContext::scientific(),
186            ..ExecContext::conservative()
187        }
188    }
189
190    /// Check cancellation and deadline.
191    pub fn check(&self) -> Result<(), EngineError> {
192        self.cancellation.check()?;
193        // `deadline` is only ever `Some` on platforms with a monotonic clock;
194        // WASM leaves it `None` and relies on budgets and cancellation.
195        if let Some(deadline) = self.deadline
196            && Instant::now() > deadline
197        {
198            return Err(EngineError::new(
199                ErrorCode::ResourceLimit,
200                format!(
201                    "request exceeded the {} ms time budget",
202                    self.limits.request_timeout_ms
203                ),
204            ));
205        }
206        Ok(())
207    }
208
209    pub fn effective(&self) -> EffectiveContext {
210        EffectiveContext {
211            numeric: self.numeric.clone(),
212            budget: self.budget,
213            seed: self.seed,
214            limits: AppliedLimits::from(&self.limits),
215            conversions: Vec::new(),
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn effective_context_serializes() {
226        let ctx = ExecContext::conservative();
227        let effective = ctx.effective();
228        let json = serde_json::to_string(&effective).unwrap();
229        assert!(json.contains("\"mode\":\"auto\""));
230    }
231}