1use 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 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 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 pub semantic_reads_unattested: bool,
114 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 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
230fn 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 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 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}