all_is_cubes/block/eval/
control.rs

1//! Details of block evaluation control flow, such as [`EvalFilter`], [`Budget`],
2//! and errors.
3
4use core::cell::Cell;
5use core::fmt;
6
7use crate::block::{
8    self, Block, BlockAttributes, BlockChange, EvaluatedBlock, Evoxel, Evoxels, Resolution,
9};
10use crate::content::palette;
11use crate::listen;
12use crate::math::{GridAab, Rgba, Vol};
13use crate::universe::{HandleError, ReadTicket};
14
15#[cfg(doc)]
16use crate::{block::BlockDef, space::Space, universe::Handle};
17
18/// Parameters to [`Block::evaluate2()`] to choose which information to compute.
19#[derive(Clone, Debug)]
20pub(crate) struct EvalFilter<'a> {
21    /// Read access to the [`BlockDef`]s and [`Space`]s that may be referenced to by the
22    /// block.
23    pub read_ticket: ReadTicket<'a>,
24
25    /// If true, don't actually evaluate, but return a placeholder value and do listen.
26    ///
27    /// TODO: All of the use cases where this is useful should actually be replaced with
28    /// combined eval+listen, but we will also want to have a "evaluate only this region"
29    /// mode which will be somewhat analogous.
30    pub skip_eval: bool,
31
32    /// A [`Listener`] which will be notified of changes in all data sources that might
33    /// affect the evaluation result.
34    ///
35    /// Note that this does not listen for mutations of the [`Block`] value itself, in the
36    /// sense that none of the methods on [`Block`] will cause this listener to fire.
37    /// Rather, it listens for changes in by-reference-to-interior-mutable-data sources
38    /// such as the [`Space`] referred to by a [`Primitive::Recur`] or the [`BlockDef`]
39    /// referred to by a [`Primitive::Indirect`].
40    pub listener: Option<listen::DynListener<BlockChange>>,
41
42    /// How much computation may be spent on performing the evaluation.
43    ///
44    /// If the budget is exhausted, evaluation returns [`EvalBlockError::StackOverflow`].
45    ///
46    /// Outside of special circumstances, use [`Budget::default()`] here.
47    pub budget: Cell<Budget>,
48}
49
50impl<'a> EvalFilter<'a> {
51    /// Returns a basic `EvalFilter` which requests a complete result and installs no
52    /// listener.
53    pub fn new(read_ticket: ReadTicket<'a>) -> Self {
54        Self {
55            read_ticket,
56            skip_eval: Default::default(),
57            listener: Default::default(),
58            budget: Default::default(),
59        }
60    }
61}
62
63/// Computation budget for block evaluations.
64///
65/// This is used inside an [`EvalFilter`].
66///
67/// In principle, what we want is a time budget, but in order to offer determinism and
68/// comprehensibility, it is instead made up of multiple discrete quantities.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub(crate) struct Budget {
71    /// Number of [`Primitive`]s and [`Modifier`]s.
72    pub(crate) components: u32,
73
74    /// Number of individual voxels produced (e.g. by a [`Primitive::Recur`]) or altered
75    /// (e.g. by a [`Modifier::Composite`]).
76    pub(crate) voxels: u32,
77
78    /// Number of levels of evaluation recursion permitted.
79    ///
80    /// Recursion occurs when a primitive or modifier which itself contains a [`Block`] is
81    /// evaluated; currently, these are [`Primitive::Text`] and [`Modifier::Composite`].
82    ///
83    /// This must be set low enough to avoid Rust stack overflows which cannot be recovered from.
84    /// Unlike the other budget parameters, this is not cumulative over the entire evaluation.
85    pub(crate) recursion: u8,
86
87    /// Lowest value that `self.recursion` has ever had.
88    ///
89    /// This is tracked separately so that it can be reported afterward,
90    /// whereas the other counters are only ever decremented and so a subtraction suffices.
91    pub(crate) min_recursion: u8,
92}
93
94impl Budget {
95    pub(in crate::block) fn decrement_components(cell: &Cell<Budget>) -> Result<(), InEvalError> {
96        let mut budget = cell.get();
97        match budget.components.checked_sub(1) {
98            Some(updated) => budget.components = updated,
99            None => return Err(InEvalError::BudgetExceeded),
100        }
101        cell.set(budget);
102        Ok(())
103    }
104
105    pub(in crate::block) fn decrement_voxels(
106        cell: &Cell<Budget>,
107        amount: usize,
108    ) -> Result<(), InEvalError> {
109        let mut budget = cell.get();
110        match u32::try_from(amount).ok().and_then(|amount| budget.voxels.checked_sub(amount)) {
111            Some(updated) => budget.voxels = updated,
112            None => return Err(InEvalError::BudgetExceeded),
113        }
114        cell.set(budget);
115        Ok(())
116    }
117
118    pub(in crate::block) fn recurse(
119        cell: &Cell<Budget>,
120    ) -> Result<BudgetRecurseGuard<'_>, InEvalError> {
121        let current = cell.get();
122        let mut recursed = current;
123        match recursed.recursion.checked_sub(1) {
124            Some(updated) => {
125                recursed.recursion = updated;
126                recursed.min_recursion = recursed.min_recursion.min(updated);
127            }
128            None => return Err(InEvalError::BudgetExceeded),
129        }
130        cell.set(recursed);
131        Ok(BudgetRecurseGuard { cell })
132    }
133
134    /// Express a budget as a [`Cost`] value, for public consumption.
135    ///
136    /// The [`Budget`] type is not public, so we only use [`Cost`] even when describing budgets.
137    ///
138    /// This method is only suitable for describing initial unused budgets.
139    /// For budgets that have been consumed, use [`Cost::from_difference()`].
140    pub(crate) fn to_cost(self) -> Cost {
141        assert_eq!(
142            self.recursion, self.min_recursion,
143            "do not use `to_cost()` on used budgets"
144        );
145        Cost {
146            components: self.components,
147            voxels: self.voxels,
148            recursion: self.recursion,
149        }
150    }
151}
152
153impl Default for Budget {
154    /// Returns the standard budget for starting any evaluation.
155    fn default() -> Self {
156        let recursion = 30;
157        Self {
158            components: 1000,
159            voxels: 64 * 64 * 128,
160            recursion,
161            min_recursion: recursion,
162        }
163    }
164}
165
166#[must_use]
167pub(crate) struct BudgetRecurseGuard<'a> {
168    cell: &'a Cell<Budget>,
169}
170
171impl Drop for BudgetRecurseGuard<'_> {
172    fn drop(&mut self) {
173        let mut budget = self.cell.get();
174        budget.recursion = budget.recursion.strict_add(1);
175        self.cell.set(budget);
176    }
177}
178
179/// The cost of evaluating a [`Block`].
180///
181/// In principle, what we want is a time budget, but in order to offer determinism and
182/// comprehensibility, it is instead measured in discrete quantities
183/// such as the number of voxels processed.
184#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
185#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
186pub struct Cost {
187    /// Number of [`Primitive`]s and [`Modifier`]s evaluated.
188    pub(crate) components: u32,
189
190    /// Number of individual voxels produced (e.g. by a `Primitive::Recur`]) or altered
191    /// (e.g. by a [`Modifier::Composite`]).
192    pub(crate) voxels: u32,
193
194    /// Number of recursion levels used by the evaluation.
195    ///
196    /// Recursion occurs when a primitive or modifier which itself contains  [`Block`] is
197    /// evaluated; currently, these are [`Primitive::Text`] and `Modifier::Composite`].
198    /// If there are none of those, then this will be zero.
199    pub(crate) recursion: u8,
200}
201
202impl Cost {
203    /// Zero cost.
204    pub const ZERO: Self = {
205        Self {
206            components: 0,
207            voxels: 0,
208            recursion: 0,
209        }
210    };
211
212    /// Compute a cost from change in budget.
213    pub(crate) fn from_difference(original_budget: Budget, final_budget: Budget) -> Self {
214        let Some(new_self) = (|| {
215            Some(Self {
216                components: original_budget.components.checked_sub(final_budget.components)?,
217                voxels: original_budget.voxels.checked_sub(final_budget.voxels)?,
218                // note use of .min_recursion!
219                recursion: original_budget.recursion.checked_sub(final_budget.min_recursion)?,
220            })
221        })() else {
222            panic!("overflow computing budget difference: {final_budget:#?} - {original_budget:#?}")
223        };
224        new_self
225    }
226}
227
228/// Errors resulting from [`Block::evaluate()`].
229#[derive(Clone, Debug, Eq, Hash, PartialEq)]
230pub struct EvalBlockError {
231    /// The block whose evaluation failed.
232    pub(crate) block: Block,
233
234    /// Computation budget that was available for the evaluation.
235    //---
236    // Design note: This field is not of type `Budget` because `Budget` is private, and
237    // structured to support its use *during* evaluation.
238    pub(crate) budget: Cost,
239
240    /// Computation steps actually used before the error was encountered.
241    pub(crate) used: Cost,
242
243    /// What specific failure was encountered.
244    pub(crate) kind: ErrorKind,
245}
246
247#[derive(Clone, Debug, Eq, Hash, PartialEq)]
248#[non_exhaustive]
249// TODO: should this be public? It may have been private by accident.
250pub(crate) enum ErrorKind {
251    /// The evaluation budget was exceeded.
252    BudgetExceeded,
253
254    /// The evaluation budget was exceeded, in a previous cached evaluation.
255    /// rather than the current one (so the current evaluation's budget
256    /// could not have affected the outcome).
257    PriorBudgetExceeded {
258        /// Budget that was available for the prior evaluation.
259        budget: Cost,
260        /// Computation steps actually used before failure of the prior evaluation.
261        used: Cost,
262    },
263
264    /// The block definition contained a [`Handle`] which was not currently available to read.
265    ///
266    /// This may be temporary or permanent; consult the [`HandleError`] to determine that.
267    Handle(HandleError),
268}
269
270/// Intra-evaluation error type; corresponds to [`EvalBlockError`]
271/// as `MinEval` corresponds to `EvaluatedBlock`.
272///
273/// TODO: This seems no longer needed since it has ended up identical.
274#[derive(Debug)]
275pub(in crate::block) enum InEvalError {
276    BudgetExceeded,
277    PriorBudgetExceeded { budget: Cost, used: Cost },
278    Handle(HandleError),
279}
280
281impl fmt::Display for EvalBlockError {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self.kind {
284            ErrorKind::BudgetExceeded => {
285                let Self { budget, used, .. } = self;
286                write!(
287                    f,
288                    "block definition exceeded evaluation budget; \
289                    used {used:?} so far and only {budget:?} available"
290                )
291            }
292            ErrorKind::PriorBudgetExceeded { budget, used } => write!(
293                f,
294                "cached block definition exceeded evaluation budget; \
295                used {used:?} so far and only {budget:?} available"
296            ),
297            ErrorKind::Handle(_) => write!(f, "block data inaccessible"),
298        }
299    }
300}
301
302impl core::error::Error for EvalBlockError {
303    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
304        match &self.kind {
305            ErrorKind::BudgetExceeded => None,
306            ErrorKind::PriorBudgetExceeded { .. } => None,
307            ErrorKind::Handle(e) => Some(e),
308        }
309    }
310}
311
312impl From<HandleError> for InEvalError {
313    fn from(value: HandleError) -> Self {
314        InEvalError::Handle(value)
315    }
316}
317
318impl InEvalError {
319    pub(in crate::block) fn into_eval_error(
320        self,
321        block: Block,
322        budget: Cost,
323        used: Cost,
324    ) -> EvalBlockError {
325        EvalBlockError {
326            block,
327            budget,
328            used,
329            kind: match self {
330                InEvalError::BudgetExceeded => ErrorKind::BudgetExceeded,
331                #[expect(clippy::shadow_unrelated)]
332                InEvalError::PriorBudgetExceeded { budget, used } => {
333                    ErrorKind::PriorBudgetExceeded { budget, used }
334                }
335                InEvalError::Handle(e) => ErrorKind::Handle(e),
336            },
337        }
338    }
339}
340
341impl EvalBlockError {
342    /// Returns the block whose evaluation failed.
343    pub fn block(&self) -> &Block {
344        &self.block
345    }
346
347    pub(in crate::block) fn into_internal_error_for_block_def(self) -> InEvalError {
348        match self.kind {
349            ErrorKind::PriorBudgetExceeded { budget, used } => {
350                InEvalError::PriorBudgetExceeded { budget, used }
351            }
352            ErrorKind::BudgetExceeded => InEvalError::BudgetExceeded,
353            ErrorKind::Handle(e) => InEvalError::Handle(e),
354        }
355    }
356
357    #[doc(hidden)] // TODO(read_ticket): eventually stop needing this
358    pub fn is_wrong_universe(&self) -> bool {
359        matches!(
360            self.kind,
361            ErrorKind::Handle(ref e) if e.is_wrong_universe()
362        )
363    }
364
365    /// Returns whether this error relates to the timing or [`ReadTicket`] used to evaluate the
366    /// block, rather than the current state of the block’s definition or the evaluation budget.
367    ///
368    /// If `true`, then reevaluating later or with a more proper [`ReadTicket`] may succeed.
369    pub(crate) fn is_transient(&self) -> bool {
370        match self.kind {
371            ErrorKind::Handle(ref h) => h.is_transient(),
372            ErrorKind::BudgetExceeded => false,
373            ErrorKind::PriorBudgetExceeded { .. } => false,
374        }
375    }
376
377    /// Convert this error into an [`EvaluatedBlock`] which represents that an error has
378    /// occurred.
379    ///
380    /// This block is fully opaque and as inert to game mechanics as currently possible.
381    // TODO: test this
382    pub fn to_placeholder(&self) -> EvaluatedBlock {
383        let resolution = Resolution::R8;
384        // TODO: indicate type of error or at least have some kind of icon,
385        let pattern = [palette::BLOCK_EVAL_ERROR, Rgba::BLACK].map(Evoxel::from_color);
386
387        EvaluatedBlock::from_voxels(
388            block::AIR, // TODO: wrong value. should get block through self
389            BlockAttributes {
390                display_name: format!("Block error: {self}").into(),
391                selectable: false, // TODO: make this selectable but immutable
392                ..Default::default()
393            },
394            Evoxels::from_many(
395                resolution,
396                Vol::from_fn(GridAab::for_block(resolution), |cube| {
397                    pattern[((cube.x + cube.y + cube.z).rem_euclid(2)) as usize]
398                }),
399            ),
400            self.used,
401        )
402    }
403}
404
405/// Convert intermediate evaluation result to final evaluation result,
406/// including calculating the evaluation cost.
407///
408/// Note that the order of operands is such that the original budget may
409/// be passed in the form `filter.budget.get()` without a temporary variable.
410pub(in crate::block) fn finish_evaluation(
411    block: Block,
412    original_budget: Budget,
413    result: Result<block::MinEval, InEvalError>,
414    filter: &EvalFilter<'_>,
415) -> Result<EvaluatedBlock, EvalBlockError> {
416    let cost = Cost::from_difference(original_budget, filter.budget.get());
417    match result {
418        Ok(ev) => Ok(ev.finish(block, cost)),
419        Err(err) => Err(err.into_eval_error(block, original_budget.to_cost(), cost)),
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    /// Most cost-tracking tests are block evaluation tests, but recursion is subtler than
428    /// the other cases and had a bug this is a regression test for.
429    #[test]
430    fn tracking_recursion_cost() {
431        let cell = Cell::new(Budget::default());
432        assert_eq!((cell.get().recursion, cell.get().min_recursion), (30, 30));
433
434        {
435            let _guard1: BudgetRecurseGuard<'_> = Budget::recurse(&cell).unwrap();
436            assert_eq!((cell.get().recursion, cell.get().min_recursion), (29, 29));
437            {
438                let _guard2: BudgetRecurseGuard<'_> = Budget::recurse(&cell).unwrap();
439                assert_eq!((cell.get().recursion, cell.get().min_recursion), (28, 28));
440            }
441        }
442        assert_eq!((cell.get().recursion, cell.get().min_recursion), (30, 28));
443
444        let cost = Cost::from_difference(Budget::default(), cell.get());
445        assert_eq!(cost.recursion, 2);
446    }
447}