1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum Budget {
23 Fast,
25 Balanced,
27 Precise,
29}
30
31impl Budget {
32 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum TraceLevel {
54 #[default]
56 Off,
57 Summary,
59 Full,
61}
62
63#[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#[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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100pub struct EffectiveContext {
101 pub numeric: NumericContext,
102 #[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#[derive(Clone, Debug)]
116pub struct ExecContext {
117 pub numeric: NumericContext,
118 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 pub fn check(&self) -> Result<(), EngineError> {
192 self.cancellation.check()?;
193 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}