Skip to main content

formualizer_eval/engine/
affected_closure.rs

1//! Request-bounded relation reachability for source publication policy.
2//!
3//! This reports maintained static edges plus eligible installed grid-read
4//! rectangles. It does not certify all semantic read kinds or decide policy.
5
6use super::authority::{geom::Rect, level_index::QueryWorkBudget, store::AuthorityError};
7use super::inspect::StateStamp;
8use super::vertex::VertexId;
9use crate::CellRef;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum AffectedRoot {
13    Cell(CellRef),
14    Symbol(VertexId),
15    Region(RegionPiece),
16    CommittedSpill(VertexId),
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct RegionPiece {
21    pub sheet: u16,
22    pub rect: Rect,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ClosureLimits {
27    pub max_work: u64,
28    pub max_output_items: usize,
29    /// Requested owned vector capacity, including outputs and relocation peaks.
30    /// Allocator metadata, excess physical allocation and total RSS are excluded.
31    pub max_scratch_bytes: usize,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ClosureResource {
36    Work,
37    Output,
38    Scratch,
39}
40
41#[derive(Debug)]
42pub enum ClosureError {
43    Authority(AuthorityError),
44    Busy,
45    InvalidRoot {
46        index: usize,
47    },
48    InconsistentIdentity,
49    StaleSnapshot,
50    Limit {
51        resource: ClosureResource,
52        limit: u64,
53        attempted: u64,
54    },
55    ArithmeticOverflow,
56    Allocation,
57}
58impl From<AuthorityError> for ClosureError {
59    fn from(error: AuthorityError) -> Self {
60        Self::Authority(error)
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct ClosureStamp {
66    pub state: StateStamp,
67    pub authority_revision: u64,
68    pub symbol_revision: u64,
69    pub observed_value_revision: u64,
70    pub semantic_revision: u64,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ReadRelationCoverage {
75    NotQueried,
76    StaticAuthorityAndCommittedGridRectangles,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum UnknownReason {
81    MissingObservation,
82    UncommittedObservation,
83    DirtyReader,
84    OpaqueStaticRelation,
85    RuntimeSymbolsUnrepresented,
86    UnavailableSpillGeometry,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct UnknownFact {
91    pub region: RegionPiece,
92    pub vertex: Option<VertexId>,
93    pub reason: UnknownReason,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
97pub struct ClosureUsage {
98    pub work: u64,
99    pub output_items: usize,
100    pub requested_capacity_peak_bytes: usize,
101}
102
103#[derive(Debug)]
104pub struct CurrentReadClosure<'engine> {
105    // Keep the source engine immutably borrowed through the policy decision.
106    owner: &'engine std::sync::Arc<()>,
107    pub stamp: ClosureStamp,
108    pub witnessed: Vec<RegionPiece>,
109    pub uncertain_downstream: Vec<RegionPiece>,
110    pub unknowns: Vec<UnknownFact>,
111    pub coverage: ReadRelationCoverage,
112    /// Other semantic read kinds are not certified by rectangle recording.
113    pub semantic_reads_unattested: bool,
114    /// A dirty or unavailable spill cannot certify prospective output geometry.
115    pub unlocalized_spill_geometry: bool,
116    pub usage: ClosureUsage,
117}
118
119impl CurrentReadClosure<'_> {
120    pub fn belongs_to<R: crate::traits::EvaluationContext>(
121        &self,
122        engine: &super::Engine<R>,
123    ) -> bool {
124        std::sync::Arc::ptr_eq(self.owner, engine.affected_closure_owner())
125    }
126}
127
128struct Budget {
129    limits: ClosureLimits,
130    usage: ClosureUsage,
131    live_bytes: usize,
132}
133impl Budget {
134    fn new(limits: ClosureLimits) -> Self {
135        Self {
136            limits,
137            usage: ClosureUsage::default(),
138            live_bytes: 0,
139        }
140    }
141    fn output(&mut self) -> Result<(), ClosureError> {
142        let attempted = self
143            .usage
144            .output_items
145            .checked_add(1)
146            .ok_or(ClosureError::ArithmeticOverflow)?;
147        if attempted > self.limits.max_output_items {
148            return Err(ClosureError::Limit {
149                resource: ClosureResource::Output,
150                limit: self.limits.max_output_items as u64,
151                attempted: attempted as u64,
152            });
153        }
154        self.usage.output_items = attempted;
155        Ok(())
156    }
157    fn push<T>(&mut self, vector: &mut Vec<T>, value: T) -> Result<(), ClosureError> {
158        self.charge_work(1)?;
159        let size = std::mem::size_of::<T>();
160        if size != 0 && vector.len() == vector.capacity() {
161            let old = vector
162                .capacity()
163                .checked_mul(size)
164                .ok_or(ClosureError::ArithmeticOverflow)?;
165            let capacity = vector
166                .capacity()
167                .max(1)
168                .checked_mul(2)
169                .ok_or(ClosureError::ArithmeticOverflow)?;
170            let requested = capacity
171                .checked_mul(size)
172                .ok_or(ClosureError::ArithmeticOverflow)?;
173            let peak = self
174                .live_bytes
175                .checked_add(requested)
176                .ok_or(ClosureError::ArithmeticOverflow)?;
177            if peak > self.limits.max_scratch_bytes {
178                return Err(ClosureError::Limit {
179                    resource: ClosureResource::Scratch,
180                    limit: self.limits.max_scratch_bytes as u64,
181                    attempted: peak as u64,
182                });
183            }
184            self.usage.requested_capacity_peak_bytes =
185                self.usage.requested_capacity_peak_bytes.max(peak);
186            vector
187                .try_reserve_exact(capacity - vector.len())
188                .map_err(|_| ClosureError::Allocation)?;
189            // The contract bounds requested capacity, not physical allocator
190            // behavior. Refuse an allocator-reported excess instead of silently
191            // using unadmitted slots in a successful report.
192            if vector.capacity() > capacity {
193                return Err(ClosureError::Allocation);
194            }
195            self.live_bytes = self
196                .live_bytes
197                .checked_sub(old)
198                .and_then(|bytes| bytes.checked_add(requested))
199                .ok_or(ClosureError::ArithmeticOverflow)?;
200        }
201        vector.push(value);
202        Ok(())
203    }
204    fn release<T>(&mut self, vector: Vec<T>) {
205        let bytes = vector.capacity() * std::mem::size_of::<T>();
206        self.live_bytes -= bytes;
207        drop(vector);
208    }
209}
210impl QueryWorkBudget for Budget {
211    type Error = ClosureError;
212    fn charge_work(&mut self, units: u64) -> Result<(), ClosureError> {
213        let attempted = self
214            .usage
215            .work
216            .checked_add(units)
217            .ok_or(ClosureError::ArithmeticOverflow)?;
218        if attempted > self.limits.max_work {
219            return Err(ClosureError::Limit {
220                resource: ClosureResource::Work,
221                limit: self.limits.max_work,
222                attempted,
223            });
224        }
225        self.usage.work = attempted;
226        Ok(())
227    }
228}
229
230/// Add only uncovered geometry, without expanding columns/cells or using a
231/// second maintained index. Temporary fragmentation shares the same admission.
232fn add_region(
233    piece: RegionPiece,
234    coverage: &mut Vec<RegionPiece>,
235    frontier: &mut Vec<RegionPiece>,
236    budget: &mut Budget,
237) -> Result<(), ClosureError> {
238    let mut fragments = Vec::new();
239    budget.push(&mut fragments, piece)?;
240    for covered in coverage.iter() {
241        budget.charge_work(1)?;
242        if covered.sheet != piece.sheet {
243            continue;
244        }
245        let mut next = Vec::new();
246        for fragment in &fragments {
247            budget.charge_work(1)?;
248            for &rect in fragment.rect.subtract(&covered.rect).as_slice() {
249                budget.push(
250                    &mut next,
251                    RegionPiece {
252                        sheet: piece.sheet,
253                        rect,
254                    },
255                )?;
256            }
257        }
258        budget.release(fragments);
259        fragments = next;
260        if fragments.is_empty() {
261            break;
262        }
263    }
264    for &fragment in &fragments {
265        budget.output()?;
266        budget.push(coverage, fragment)?;
267        budget.push(frontier, fragment)?;
268    }
269    budget.release(fragments);
270    Ok(())
271}
272
273struct Reader<'engine> {
274    vertex: VertexId,
275    region: RegionPiece,
276    reads: Option<&'engine [super::authority::host::ObservedRect]>,
277}
278
279fn add_unknown(
280    fact: UnknownFact,
281    unknowns: &mut Vec<UnknownFact>,
282    budget: &mut Budget,
283) -> Result<(), ClosureError> {
284    for known in unknowns.iter() {
285        budget.charge_work(1)?;
286        if *known == fact {
287            return Ok(());
288        }
289    }
290    budget.output()?;
291    budget.push(unknowns, fact)
292}
293
294impl<R: crate::traits::EvaluationContext> super::Engine<R> {
295    fn affected_readers<'engine>(
296        &'engine self,
297        store: &super::authority::store::Store,
298        unknowns: &mut Vec<UnknownFact>,
299        budget: &mut Budget,
300    ) -> Result<Vec<Reader<'engine>>, ClosureError> {
301        use super::authority::store::{F_DYNAMIC, F_OPAQUE};
302        let mut readers = Vec::new();
303        store.visit_owner_slots_bounded(budget, &mut |sheet, domain, flags, budget| {
304            if flags & F_OPAQUE != 0 {
305                add_unknown(
306                    UnknownFact {
307                        region: RegionPiece {
308                            sheet,
309                            rect: domain,
310                        },
311                        vertex: None,
312                        reason: UnknownReason::OpaqueStaticRelation,
313                    },
314                    unknowns,
315                    budget,
316                )?;
317            }
318            if flags & F_DYNAMIC == 0 {
319                return Ok(());
320            }
321            for row in domain.r0..=domain.r1 {
322                for column in domain.c0..=domain.c1 {
323                    // Conservative admission for fixed-width numeric B-tree
324                    // predecessor and identity lookups, not exact comparisons.
325                    budget.charge_work(128)?;
326                    let coordinate = (sheet, row, column);
327                    let vertex = self
328                        .graph
329                        .authority_vertex_of_cell(coordinate)
330                        .ok_or(ClosureError::InconsistentIdentity)?;
331                    if !self.graph.vertex_exists_active(vertex)
332                        || self.graph.authority_cell_of_vertex(vertex) != Some(coordinate)
333                    {
334                        return Err(ClosureError::InconsistentIdentity);
335                    }
336                    let region = RegionPiece {
337                        sheet,
338                        rect: Rect::cell(row, column),
339                    };
340                    let host = self.graph.authority_host();
341                    let committed = host.committed_observed(vertex);
342                    let dirty = self.graph.is_dirty(vertex);
343                    let reason = if dirty {
344                        Some(UnknownReason::DirtyReader)
345                    } else if committed.is_none() {
346                        Some(if host.observed(vertex).is_some() {
347                            UnknownReason::UncommittedObservation
348                        } else {
349                            UnknownReason::MissingObservation
350                        })
351                    } else {
352                        None
353                    };
354                    if let Some(reason) = reason {
355                        add_unknown(
356                            UnknownFact {
357                                region,
358                                vertex: Some(vertex),
359                                reason,
360                            },
361                            unknowns,
362                            budget,
363                        )?;
364                    }
365                    let reads = if dirty { None } else { committed };
366                    if let Some(reads) = reads {
367                        for &(read_sheet, r0, c0, r1, c1) in reads {
368                            budget.charge_work(1)?;
369                            if !self.affected_region_valid(RegionPiece {
370                                sheet: read_sheet,
371                                rect: Rect { r0, c0, r1, c1 },
372                            }) {
373                                return Err(ClosureError::InconsistentIdentity);
374                            }
375                        }
376                    }
377                    budget.push(
378                        &mut readers,
379                        Reader {
380                            vertex,
381                            region,
382                            reads,
383                        },
384                    )?;
385                }
386            }
387            Ok(())
388        })?;
389        Ok(readers)
390    }
391
392    fn walk_affected(
393        &self,
394        store: &super::authority::store::Store,
395        readers: &[Reader<'_>],
396        coverage: &mut Vec<RegionPiece>,
397        frontier: &mut Vec<RegionPiece>,
398        unknowns: &mut Vec<UnknownFact>,
399        unlocalized_spill_geometry: &mut bool,
400        budget: &mut Budget,
401    ) -> Result<(), ClosureError> {
402        while let Some(piece) = frontier.pop() {
403            budget.charge_work(1)?;
404            store.visit_direct_dependents_bounded(
405                piece.sheet,
406                &piece.rect,
407                super::authority::store::TagFilter::All,
408                budget,
409                &mut |sheet, rect, budget| {
410                    add_region(RegionPiece { sheet, rect }, coverage, frontier, budget)
411                },
412            )?;
413            for reader in readers {
414                budget.charge_work(1)?;
415                if piece.sheet == super::authority::geom::SYMBOL_SHEET {
416                    add_unknown(
417                        UnknownFact {
418                            region: reader.region,
419                            vertex: Some(reader.vertex),
420                            reason: UnknownReason::RuntimeSymbolsUnrepresented,
421                        },
422                        unknowns,
423                        budget,
424                    )?;
425                }
426                if let Some(reads) = reader.reads {
427                    for &(sheet, r0, c0, r1, c1) in reads {
428                        budget.charge_work(1)?;
429                        if piece.sheet == sheet && piece.rect.intersects(&Rect { r0, c0, r1, c1 }) {
430                            add_region(reader.region, coverage, frontier, budget)?;
431                            break;
432                        }
433                    }
434                }
435            }
436            self.graph
437                .visit_spill_anchors_bounded(budget, &mut |anchor, budget| {
438                    budget.charge_work(128)?;
439                    let cell = self
440                        .graph
441                        .get_cell_ref(anchor)
442                        .ok_or(ClosureError::InconsistentIdentity)?;
443                    if cell.sheet_id != piece.sheet
444                        || !piece.rect.contains(cell.coord.row(), cell.coord.col())
445                    {
446                        return Ok(());
447                    }
448                    let extent = self.graph.spill_extent_for_anchor(anchor);
449                    if self.graph.is_dirty(anchor)
450                        || self.graph.spill_may_be_intruded(anchor)
451                        || extent.is_none()
452                        || extent.is_some_and(|(first, last)| {
453                            first != cell || last.sheet_id != cell.sheet_id
454                        })
455                    {
456                        *unlocalized_spill_geometry = true;
457                        return add_unknown(
458                            UnknownFact {
459                                region: RegionPiece {
460                                    sheet: cell.sheet_id,
461                                    rect: Rect::cell(cell.coord.row(), cell.coord.col()),
462                                },
463                                vertex: Some(anchor),
464                                reason: UnknownReason::UnavailableSpillGeometry,
465                            },
466                            unknowns,
467                            budget,
468                        );
469                    }
470                    let (first, last) = extent.unwrap();
471                    let region = RegionPiece {
472                        sheet: first.sheet_id,
473                        rect: Rect::new(
474                            first.coord.row(),
475                            first.coord.col(),
476                            last.coord.row(),
477                            last.coord.col(),
478                        ),
479                    };
480                    if !self.affected_region_valid(region) {
481                        return Err(ClosureError::InconsistentIdentity);
482                    }
483                    add_region(region, coverage, frontier, budget)
484                })?;
485        }
486        Ok(())
487    }
488}
489
490impl<R: crate::traits::EvaluationContext> super::Engine<R> {
491    fn affected_closure_stamp(&self) -> ClosureStamp {
492        let host = self.graph.authority_host();
493        ClosureStamp {
494            state: StateStamp {
495                mutation_revision: self.inspection_mutation_revision(),
496                recalc_epoch: self.recalc_epoch,
497            },
498            authority_revision: host.revision,
499            symbol_revision: self.graph.symbol_revision(),
500            observed_value_revision: host.observed_value_revision(),
501            semantic_revision: crate::function_registry::semantic_epoch_lock_free(),
502        }
503    }
504
505    fn affected_region_valid(&self, region: RegionPiece) -> bool {
506        region.sheet != super::authority::geom::SYMBOL_SHEET
507            && !self.graph.sheet_name(region.sheet).is_empty()
508            && region.rect.r0 <= region.rect.r1
509            && region.rect.c0 <= region.rect.c1
510            && region.rect.r1 < 1_048_576
511            && region.rect.c1 < 16_384
512    }
513
514    /// Immutable, request-local relation query. Output borrows the engine so a
515    /// policy decision cannot retain it across a safe mutable engine operation.
516    /// Negative facts apply only to the reported relation and declared unknowns.
517    pub fn current_read_affected_closure(
518        &self,
519        roots: &[AffectedRoot],
520        limits: ClosureLimits,
521    ) -> Result<CurrentReadClosure<'_>, ClosureError> {
522        let stamp = self.affected_closure_stamp();
523        if roots.is_empty() {
524            return Ok(CurrentReadClosure {
525                owner: self.affected_closure_owner(),
526                stamp,
527                witnessed: Vec::new(),
528                uncertain_downstream: Vec::new(),
529                unknowns: Vec::new(),
530                coverage: ReadRelationCoverage::NotQueried,
531                semantic_reads_unattested: true,
532                unlocalized_spill_geometry: false,
533                usage: ClosureUsage::default(),
534            });
535        }
536        let store = self.graph.authority_plan_store()?;
537        let host = self.graph.authority_host();
538        if !self.affected_closure_idle() || host.structural_pending || host.replaying {
539            return Err(ClosureError::Busy);
540        }
541        let mut budget = Budget::new(limits);
542        let (mut witnessed, mut frontier) = (Vec::new(), Vec::new());
543        let mut unknowns = Vec::new();
544        let mut unlocalized_spill_geometry = false;
545        for (index, root) in roots.iter().enumerate() {
546            budget.charge_work(1)?;
547            let region = match *root {
548                AffectedRoot::Cell(cell) => RegionPiece {
549                    sheet: cell.sheet_id,
550                    rect: Rect::cell(cell.coord.row(), cell.coord.col()),
551                },
552                AffectedRoot::Region(region) => region,
553                AffectedRoot::Symbol(vertex) => {
554                    budget.charge_work(128)?;
555                    if !self.graph.vertex_exists_active(vertex) {
556                        return Err(ClosureError::InvalidRoot { index });
557                    }
558                    let Some((sheet, row, column)) = self.graph.authority_cell_of_vertex(vertex)
559                    else {
560                        return Err(ClosureError::InvalidRoot { index });
561                    };
562                    if sheet != super::authority::geom::SYMBOL_SHEET {
563                        return Err(ClosureError::InvalidRoot { index });
564                    }
565                    add_region(
566                        RegionPiece {
567                            sheet,
568                            rect: Rect::cell(row, column),
569                        },
570                        &mut witnessed,
571                        &mut frontier,
572                        &mut budget,
573                    )?;
574                    continue;
575                }
576                AffectedRoot::CommittedSpill(vertex) => {
577                    budget.charge_work(128)?;
578                    if !self.graph.is_spill_anchor(vertex)
579                        || !self.graph.is_live_formula_vertex(vertex)
580                    {
581                        return Err(ClosureError::InvalidRoot { index });
582                    }
583                    let cell = self
584                        .graph
585                        .get_cell_ref(vertex)
586                        .ok_or(ClosureError::InvalidRoot { index })?;
587                    RegionPiece {
588                        sheet: cell.sheet_id,
589                        rect: Rect::cell(cell.coord.row(), cell.coord.col()),
590                    }
591                }
592            };
593            if !self.affected_region_valid(region) {
594                return Err(ClosureError::InvalidRoot { index });
595            }
596            add_region(region, &mut witnessed, &mut frontier, &mut budget)?;
597        }
598        let readers = self.affected_readers(store, &mut unknowns, &mut budget)?;
599        self.walk_affected(
600            store,
601            &readers,
602            &mut witnessed,
603            &mut frontier,
604            &mut unknowns,
605            &mut unlocalized_spill_geometry,
606            &mut budget,
607        )?;
608        let mut uncertain_downstream = Vec::new();
609        let mut seeded = 0;
610        loop {
611            while seeded < unknowns.len() {
612                budget.charge_work(1)?;
613                let region = unknowns[seeded].region;
614                seeded += 1;
615                add_region(
616                    region,
617                    &mut uncertain_downstream,
618                    &mut frontier,
619                    &mut budget,
620                )?;
621            }
622            if frontier.is_empty() {
623                break;
624            }
625            self.walk_affected(
626                store,
627                &readers,
628                &mut uncertain_downstream,
629                &mut frontier,
630                &mut unknowns,
631                &mut unlocalized_spill_geometry,
632                &mut budget,
633            )?;
634        }
635        budget.charge_work(1)?;
636        if self.affected_closure_stamp() != stamp {
637            return Err(ClosureError::StaleSnapshot);
638        }
639        budget.release(frontier);
640        budget.charge_work(readers.len() as u64)?;
641        budget.release(readers);
642        Ok(CurrentReadClosure {
643            owner: self.affected_closure_owner(),
644            stamp,
645            witnessed,
646            uncertain_downstream,
647            unknowns,
648            coverage: ReadRelationCoverage::StaticAuthorityAndCommittedGridRectangles,
649            semantic_reads_unattested: true,
650            unlocalized_spill_geometry,
651            usage: budget.usage,
652        })
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    fn limits() -> ClosureLimits {
660        ClosureLimits {
661            max_work: 10_000,
662            max_output_items: 100,
663            max_scratch_bytes: 64 * 1024,
664        }
665    }
666    fn piece(rect: Rect) -> RegionPiece {
667        RegionPiece { sheet: 0, rect }
668    }
669    #[test]
670    fn affected_closure_cover_keeps_exact_disjoint_sparse_geometry() {
671        let mut budget = Budget::new(limits());
672        let (mut cover, mut queue) = (Vec::new(), Vec::new());
673        add_region(
674            piece(Rect::new(1, 1, 3, 3)),
675            &mut cover,
676            &mut queue,
677            &mut budget,
678        )
679        .unwrap();
680        add_region(
681            piece(Rect::new(2, 2, 4, 4)),
682            &mut cover,
683            &mut queue,
684            &mut budget,
685        )
686        .unwrap();
687        add_region(
688            piece(Rect::new(1, 1, 3, 3)),
689            &mut cover,
690            &mut queue,
691            &mut budget,
692        )
693        .unwrap();
694        assert_eq!(cover.iter().map(|item| item.rect.area()).sum::<u64>(), 14);
695        for (i, item) in cover.iter().enumerate() {
696            for other in &cover[i + 1..] {
697                assert!(!item.rect.intersects(&other.rect));
698            }
699        }
700        add_region(
701            piece(Rect::cell(1_048_575, 16_383)),
702            &mut cover,
703            &mut queue,
704            &mut budget,
705        )
706        .unwrap();
707        assert_eq!(cover.iter().map(|item| item.rect.area()).sum::<u64>(), 15);
708        assert!(cover.len() <= 5);
709        assert!(budget.usage.requested_capacity_peak_bytes <= limits().max_scratch_bytes);
710    }
711    #[test]
712    fn affected_closure_limits_deny_before_vector_insertion() {
713        for resource in [
714            ClosureResource::Work,
715            ClosureResource::Output,
716            ClosureResource::Scratch,
717        ] {
718            let mut cap = limits();
719            match resource {
720                ClosureResource::Work => cap.max_work = 0,
721                ClosureResource::Output => cap.max_output_items = 0,
722                ClosureResource::Scratch => cap.max_scratch_bytes = 0,
723            }
724            let mut budget = Budget::new(cap);
725            let (mut cover, mut queue) = (Vec::new(), Vec::new());
726            assert!(
727                matches!(add_region(piece(Rect::cell(0, 0)), &mut cover, &mut queue, &mut budget),
728                Err(ClosureError::Limit { resource: found, .. }) if found == resource)
729            );
730            assert!(cover.is_empty() && queue.is_empty());
731            assert!(budget.usage.work <= cap.max_work);
732            assert!(budget.usage.requested_capacity_peak_bytes <= cap.max_scratch_bytes);
733        }
734    }
735    #[test]
736    fn affected_closure_scratch_accounts_relocation_and_both_owned_outputs() {
737        let mut roomy = Budget::new(limits());
738        let (mut cover, mut queue) = (Vec::new(), Vec::new());
739        add_region(piece(Rect::cell(0, 0)), &mut cover, &mut queue, &mut roomy).unwrap();
740        let needed = roomy.usage.requested_capacity_peak_bytes;
741        assert!(needed >= 6 * std::mem::size_of::<RegionPiece>());
742        for cap in [needed - 1, needed] {
743            let mut restricted = limits();
744            restricted.max_scratch_bytes = cap;
745            let mut budget = Budget::new(restricted);
746            let (mut cover, mut queue) = (Vec::new(), Vec::new());
747            let result = add_region(piece(Rect::cell(0, 0)), &mut cover, &mut queue, &mut budget);
748            assert_eq!(result.is_ok(), cap == needed);
749            assert!(budget.usage.requested_capacity_peak_bytes <= cap);
750        }
751    }
752}