1use std::cell::RefCell;
9use std::collections::VecDeque;
10use std::hash::Hash;
11use std::rc::Rc;
12
13use bolero::generator::bolero_generator::driver::object::Borrowed;
14use bolero::{ValueGenerator, produce};
15use dfir_rs::rustc_hash::FxHashMap;
16use dfir_rs::util::unsync::mpsc::Sender;
17
18use super::{
19 HookLocationMeta, ManualDebug, RuntimeHook, ScriptDecision, ScriptableHook,
20 ScriptableTickInputHook, TickInputHook, TruncatedVecDebug, abort, log_release,
21};
22use crate::live_collections::stream::{NoOrder, Ordering, TotalOrder};
23
24pub struct StreamHook<T, Order: Ordering> {
25 pub input: Rc<RefCell<VecDeque<T>>>,
26 pub to_release: Option<Vec<T>>,
27 pub output: Sender<T>,
28 pub batch_location: HookLocationMeta,
29 pub format_item_debug: fn(&T) -> Option<String>,
30 pub _order: std::marker::PhantomData<Order>,
31}
32
33impl<T> RuntimeHook for StreamHook<T, TotalOrder> {
34 fn has_pending_input(&self) -> bool {
35 !self.input.borrow().is_empty()
36 }
37
38 fn only_one_possible_decision(&self) -> bool {
39 self.input.borrow().is_empty()
41 }
42
43 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
44 if let Some(to_release) = self.to_release.take() {
45 if let Some(log_writer) = log_writer {
46 let HookLocationMeta {
47 location: batch_location,
48 line,
49 caret_indent,
50 } = self.batch_location;
51 let note_str = if to_release.is_empty() {
52 "^ releasing no items".to_owned()
53 } else {
54 format!(
55 "^ releasing items: {:?}",
56 TruncatedVecDebug(
57 RefCell::new(Some(to_release.iter())),
58 8,
59 self.format_item_debug
60 )
61 )
62 };
63
64 log_release(
65 log_writer,
66 batch_location,
67 line,
68 caret_indent,
69 ¬e_str,
70 colored::Color::Green,
71 );
72 }
73
74 for item in to_release {
75 self.output.try_send(item).unwrap();
76 }
77 } else {
78 panic!("No decision to release");
79 }
80 }
81
82 fn location_meta(&self) -> HookLocationMeta {
83 self.batch_location
84 }
85}
86
87impl<T> TickInputHook for StreamHook<T, TotalOrder> {
88 fn can_trigger_tick(&self) -> bool {
89 !self.input.borrow().is_empty()
90 }
91
92 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
93 let mut current_input = self.input.borrow_mut();
94 let count = ((if force_trigger { 1 } else { 0 })..=current_input.len())
95 .generate(driver)
96 .unwrap();
97
98 self.to_release = Some(current_input.drain(0..count).collect());
99 count > 0
100 }
101}
102
103impl<T> RuntimeHook for StreamHook<T, NoOrder> {
104 fn has_pending_input(&self) -> bool {
105 !self.input.borrow().is_empty()
106 }
107
108 fn only_one_possible_decision(&self) -> bool {
109 self.input.borrow().is_empty()
111 }
112
113 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
114 if let Some(to_release) = self.to_release.take() {
115 if let Some(log_writer) = log_writer {
116 let HookLocationMeta {
117 location: batch_location,
118 line,
119 caret_indent,
120 } = self.batch_location;
121 let note_str = if to_release.is_empty() {
122 "^ releasing no items".to_owned()
123 } else {
124 format!(
125 "^ releasing unordered items: {:?}",
126 TruncatedVecDebug(
127 RefCell::new(Some(to_release.iter())),
128 8,
129 self.format_item_debug
130 )
131 )
132 };
133
134 log_release(
135 log_writer,
136 batch_location,
137 line,
138 caret_indent,
139 ¬e_str,
140 colored::Color::Green,
141 );
142 }
143
144 for item in to_release {
145 self.output.try_send(item).unwrap();
146 }
147 } else {
148 panic!("No decision to release");
149 }
150 }
151
152 fn location_meta(&self) -> HookLocationMeta {
153 self.batch_location
154 }
155}
156
157impl<T> TickInputHook for StreamHook<T, NoOrder> {
158 fn can_trigger_tick(&self) -> bool {
159 !self.input.borrow().is_empty()
160 }
161
162 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
163 let mut current_input = self.input.borrow_mut();
164 let mut out = vec![];
165 let mut min_index = 0;
166 while !current_input.is_empty() {
167 let must_release = force_trigger && out.is_empty();
168 if !must_release && produce().generate(driver).unwrap() {
169 break;
170 }
171
172 let idx = (min_index..current_input.len()).generate(driver).unwrap();
173 let item = current_input.remove(idx).unwrap();
174 out.push(item);
175
176 min_index = idx;
177 if min_index == current_input.len() {
182 break;
183 }
184 }
185
186 let triggered = !out.is_empty();
187 self.to_release = Some(out);
188 triggered
189 }
190}
191
192#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
194pub enum BatchDecision<T> {
195 Prefix(usize),
197 Values(Vec<T>),
199 All,
201}
202
203impl<T> ScriptDecision for BatchDecision<T>
204where
205 T: serde::Serialize + serde::de::DeserializeOwned,
206{
207 fn describe(&self) -> String {
208 match self {
209 BatchDecision::Prefix(n) => format!("release({})", n),
210 BatchDecision::Values(values) => {
211 format!("release_values({} value(s))", values.len())
212 }
213 BatchDecision::All => "release_all()".to_owned(),
214 }
215 }
216}
217
218#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
220pub enum UnorderedBatchDecision<T> {
221 Values(Vec<T>),
223 All,
225}
226
227impl<T> ScriptDecision for UnorderedBatchDecision<T>
228where
229 T: serde::Serialize + serde::de::DeserializeOwned,
230{
231 fn describe(&self) -> String {
232 match self {
233 UnorderedBatchDecision::Values(values) => {
234 format!("release_values({} value(s))", values.len())
235 }
236 UnorderedBatchDecision::All => "release_all()".to_owned(),
237 }
238 }
239}
240
241#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
244pub struct BatchStatus {
245 pub buffered: usize,
247}
248
249impl<T> ScriptableHook for StreamHook<T, TotalOrder>
250where
251 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
252{
253 type Decision = BatchDecision<T>;
254 type Status = BatchStatus;
255
256 fn is_honorable(&self, decision: &BatchDecision<T>) -> Result<bool, String> {
257 let input = self.input.borrow();
258 match decision {
259 BatchDecision::Prefix(n) => Ok(input.len() >= *n),
260 BatchDecision::Values(values) => {
261 if let Some(idx) = input
262 .iter()
263 .zip(values)
264 .position(|(buffered, expected)| buffered != expected)
265 {
266 Err(format!(
267 "release_values: buffered item at prefix position {} did not match the expected value",
268 idx
269 ))
270 } else {
271 Ok(input.len() >= values.len())
272 }
273 }
274 BatchDecision::All => Ok(true),
275 }
276 }
277
278 fn apply(&mut self, decision: BatchDecision<T>) {
279 let mut input = self.input.borrow_mut();
280 let out: Vec<T> = match decision {
281 BatchDecision::Prefix(n) => input.drain(0..n).collect(),
282 BatchDecision::Values(values) => input.drain(0..values.len()).collect(),
283 BatchDecision::All => input.drain(..).collect(),
284 };
285
286 self.to_release = Some(out);
287 }
288
289 fn implicit(&mut self) {
290 self.to_release = Some(vec![]);
291 }
292
293 fn status(&self) -> BatchStatus {
294 BatchStatus {
295 buffered: self.input.borrow().len(),
296 }
297 }
298
299 fn describe_pending(&self) -> Option<String> {
300 let input = self.input.borrow();
301 (!input.is_empty()).then(|| {
302 format!(
303 "{} buffered item(s): {:?}",
304 input.len(),
305 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
306 )
307 })
308 }
309}
310
311impl<T> ScriptableTickInputHook for StreamHook<T, TotalOrder>
312where
313 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
314{
315 fn decision_triggers_tick(&self, decision: &BatchDecision<T>) -> bool {
316 match decision {
317 BatchDecision::Prefix(n) => *n > 0,
318 BatchDecision::Values(values) => !values.is_empty(),
319 BatchDecision::All => !self.input.borrow().is_empty(),
320 }
321 }
322}
323
324impl<T> ScriptableHook for StreamHook<T, NoOrder>
325where
326 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
327{
328 type Decision = UnorderedBatchDecision<T>;
329 type Status = BatchStatus;
330
331 fn is_honorable(&self, decision: &UnorderedBatchDecision<T>) -> Result<bool, String> {
332 Ok(match decision {
333 UnorderedBatchDecision::Values(values) => {
334 let mut unmatched: Vec<&T> = values.iter().collect();
335 for buffered in self.input.borrow().iter() {
336 if let Some(idx) = unmatched
337 .iter()
338 .position(|requested| *requested == buffered)
339 {
340 unmatched.swap_remove(idx);
341 }
342 }
343 unmatched.is_empty()
344 }
345 UnorderedBatchDecision::All => true,
346 })
347 }
348
349 fn apply(&mut self, decision: UnorderedBatchDecision<T>) {
350 let mut input = self.input.borrow_mut();
351 let out: Vec<T> = match decision {
352 UnorderedBatchDecision::Values(mut values) => {
353 let (selected, remaining): (Vec<_>, Vec<_>) =
354 input.drain(..).partition(|buffered| {
355 values
356 .iter()
357 .position(|requested| requested == buffered)
358 .is_some_and(|idx| {
359 values.swap_remove(idx);
360 true
361 })
362 });
363 assert!(
364 values.is_empty(),
365 "scripted unordered batch decision was not honorable"
366 );
367 *input = remaining.into();
368 selected
369 }
370 UnorderedBatchDecision::All => input.drain(..).collect(),
371 };
372
373 self.to_release = Some(out);
374 }
375
376 fn implicit(&mut self) {
377 self.to_release = Some(vec![]);
378 }
379
380 fn status(&self) -> BatchStatus {
381 BatchStatus {
382 buffered: self.input.borrow().len(),
383 }
384 }
385
386 fn describe_pending(&self) -> Option<String> {
387 let input = self.input.borrow();
388 (!input.is_empty()).then(|| {
389 format!(
390 "{} buffered item(s): {:?}",
391 input.len(),
392 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
393 )
394 })
395 }
396}
397
398impl<T> ScriptableTickInputHook for StreamHook<T, NoOrder>
399where
400 T: serde::Serialize + serde::de::DeserializeOwned + PartialEq,
401{
402 fn decision_triggers_tick(&self, decision: &UnorderedBatchDecision<T>) -> bool {
403 match decision {
404 UnorderedBatchDecision::Values(values) => !values.is_empty(),
405 UnorderedBatchDecision::All => !self.input.borrow().is_empty(),
406 }
407 }
408}
409
410pub struct KeyedStreamHook<K: Hash + Eq + Clone, V, Order: Ordering> {
411 pub input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>, pub to_release: Option<Vec<(K, V)>>,
413 pub output: Sender<(K, V)>,
414 pub batch_location: HookLocationMeta,
415 pub format_item_debug: fn(&(K, V)) -> Option<String>,
416 pub _order: std::marker::PhantomData<Order>,
417}
418
419impl<K: Hash + Eq + Clone, V> RuntimeHook for KeyedStreamHook<K, V, TotalOrder> {
420 fn has_pending_input(&self) -> bool {
421 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
422 !self.input.borrow().values().all(|q| q.is_empty())
423 }
424
425 fn only_one_possible_decision(&self) -> bool {
426 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
428 self.input.borrow().values().all(|q| q.is_empty())
429 }
430
431 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
432 if let Some(to_release) = self.to_release.take() {
433 if let Some(log_writer) = log_writer {
434 let HookLocationMeta {
435 location: batch_location,
436 line,
437 caret_indent,
438 } = self.batch_location;
439 let note_str = if to_release.is_empty() {
440 "^ releasing no items".to_owned()
441 } else {
442 format!(
443 "^ releasing items: {:?}",
444 TruncatedVecDebug(
445 RefCell::new(Some(to_release.iter())),
446 8,
447 self.format_item_debug
448 )
449 )
450 };
451
452 log_release(
453 log_writer,
454 batch_location,
455 line,
456 caret_indent,
457 ¬e_str,
458 colored::Color::Green,
459 );
460 }
461
462 for item in to_release {
463 self.output.try_send(item).unwrap();
464 }
465 } else {
466 panic!("No decision to release");
467 }
468 }
469
470 fn location_meta(&self) -> HookLocationMeta {
471 self.batch_location
472 }
473}
474
475impl<K: Hash + Eq + Clone, V> TickInputHook for KeyedStreamHook<K, V, TotalOrder> {
476 fn can_trigger_tick(&self) -> bool {
477 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
478 !self.input.borrow().values().all(|q| q.is_empty())
479 }
480
481 fn autonomous_decision<'a>(
482 &mut self,
483 driver: &mut Borrowed<'a>,
484 mut force_trigger: bool,
485 ) -> bool {
486 let mut current_input = self.input.borrow_mut();
487 self.to_release = Some(vec![]);
488 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
489 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
490
491 let mut remaining_nonempty_keys = nonempty_key_count;
492 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
493 for (key, queue) in current_input.iter_mut() {
494 if queue.is_empty() {
495 continue;
496 }
497
498 remaining_nonempty_keys -= 1;
499
500 let count = ((if force_trigger && remaining_nonempty_keys == 0 {
501 1
502 } else {
503 0
504 })..=queue.len())
505 .generate(driver)
506 .unwrap();
507
508 let items: Vec<(K, V)> = queue.drain(0..count).map(|v| (key.clone(), v)).collect();
509 self.to_release.as_mut().unwrap().extend(items);
510
511 if count > 0 {
512 force_trigger = false;
513 }
514 }
515
516 !self.to_release.as_ref().unwrap().is_empty()
517 }
518}
519
520impl<K: Hash + Eq + Clone, V> RuntimeHook for KeyedStreamHook<K, V, NoOrder> {
521 fn has_pending_input(&self) -> bool {
522 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
523 !self.input.borrow().values().all(|q| q.is_empty())
524 }
525
526 fn only_one_possible_decision(&self) -> bool {
527 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
529 self.input.borrow().values().all(|q| q.is_empty())
530 }
531
532 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
533 if let Some(to_release) = self.to_release.take() {
534 if let Some(log_writer) = log_writer {
535 let HookLocationMeta {
536 location: batch_location,
537 line,
538 caret_indent,
539 } = self.batch_location;
540 let note_str = if to_release.is_empty() {
541 "^ releasing no items".to_owned()
542 } else {
543 format!(
544 "^ releasing unordered items: {:?}",
545 TruncatedVecDebug(
546 RefCell::new(Some(to_release.iter())),
547 8,
548 self.format_item_debug
549 )
550 )
551 };
552
553 log_release(
554 log_writer,
555 batch_location,
556 line,
557 caret_indent,
558 ¬e_str,
559 colored::Color::Green,
560 );
561 }
562
563 for item in to_release {
564 self.output.try_send(item).unwrap();
565 }
566 } else {
567 panic!("No decision to release");
568 }
569 }
570
571 fn location_meta(&self) -> HookLocationMeta {
572 self.batch_location
573 }
574}
575
576impl<K: Hash + Eq + Clone, V> TickInputHook for KeyedStreamHook<K, V, NoOrder> {
577 fn can_trigger_tick(&self) -> bool {
578 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
579 !self.input.borrow().values().all(|q| q.is_empty())
580 }
581
582 fn autonomous_decision<'a>(
583 &mut self,
584 driver: &mut Borrowed<'a>,
585 mut force_trigger: bool,
586 ) -> bool {
587 let mut current_input = self.input.borrow_mut();
588 self.to_release = Some(vec![]);
589 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
590 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
591
592 let mut remaining_nonempty_keys = nonempty_key_count;
593 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
594 for (key, queue) in current_input.iter_mut() {
595 if queue.is_empty() {
596 continue;
597 }
598
599 remaining_nonempty_keys -= 1;
600
601 let mut min_index = 0;
602 while !queue.is_empty() {
603 let must_release = force_trigger && remaining_nonempty_keys == 0;
604 if !must_release && produce().generate(driver).unwrap() {
605 break;
606 }
607
608 let idx = (min_index..queue.len()).generate(driver).unwrap();
609 let item = queue.remove(idx).unwrap();
610 self.to_release.as_mut().unwrap().push((key.clone(), item));
611 force_trigger = false;
612
613 min_index = idx;
614 if min_index == queue.len() {
619 break;
620 }
621 }
622 }
623
624 !self.to_release.as_ref().unwrap().is_empty()
625 }
626}
627
628pub struct SingletonHook<T> {
629 input: Rc<RefCell<VecDeque<T>>>,
630 to_release: Option<(T, bool)>, last_released: Option<T>,
632 skipped_states: Vec<T>,
633 output: Sender<T>,
634 batch_location: HookLocationMeta,
635 format_item_debug: fn(&T) -> Option<String>,
636}
637
638impl<T: Clone> SingletonHook<T> {
639 pub fn new(
640 input: Rc<RefCell<VecDeque<T>>>,
641 output: Sender<T>,
642 batch_location: HookLocationMeta,
643 format_item_debug: fn(&T) -> Option<String>,
644 ) -> Self {
645 Self {
646 input,
647 to_release: None,
648 last_released: None,
649 skipped_states: vec![],
650 output,
651 batch_location,
652 format_item_debug,
653 }
654 }
655}
656
657impl<T: Clone> RuntimeHook for SingletonHook<T> {
658 fn has_pending_input(&self) -> bool {
659 !self.input.borrow().is_empty()
660 }
661
662 fn only_one_possible_decision(&self) -> bool {
663 let input = self.input.borrow();
668 input.is_empty() || (input.len() == 1 && self.last_released.is_none())
669 }
670
671 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
672 if let Some((to_release, is_new)) = self.to_release.take() {
673 self.last_released = Some(to_release.clone());
674
675 if let Some(log_writer) = log_writer {
676 let HookLocationMeta {
677 location: batch_location,
678 line,
679 caret_indent,
680 } = self.batch_location;
681 let note_str = if self.skipped_states.is_empty() {
682 if is_new {
683 format!(
684 "^ releasing snapshot: {:?}",
685 ManualDebug(&to_release, self.format_item_debug)
686 )
687 } else {
688 format!(
689 "^ releasing unchanged snapshot: {:?}",
690 ManualDebug(&to_release, self.format_item_debug)
691 )
692 }
693 } else {
694 format!(
695 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
696 ManualDebug(&to_release, self.format_item_debug),
697 self.skipped_states
698 .iter()
699 .map(|s| ManualDebug(s, self.format_item_debug))
700 .collect::<Vec<_>>()
701 )
702 };
703
704 log_release(
705 log_writer,
706 batch_location,
707 line,
708 caret_indent,
709 ¬e_str,
710 colored::Color::Green,
711 );
712 }
713
714 self.output.try_send(to_release).unwrap();
715 } else {
716 panic!("No decision to release");
717 }
718 }
719
720 fn location_meta(&self) -> HookLocationMeta {
721 self.batch_location
722 }
723}
724
725impl<T: Clone> TickInputHook for SingletonHook<T> {
726 fn can_trigger_tick(&self) -> bool {
727 !self.input.borrow().is_empty()
729 }
730
731 fn autonomous_decision<'a>(&mut self, driver: &mut Borrowed<'a>, force_trigger: bool) -> bool {
732 let mut current_input = self.input.borrow_mut();
733 if current_input.is_empty() {
734 if force_trigger {
735 panic!("Cannot make a triggering decision when there is no input");
736 }
737
738 if let Some(last) = &self.last_released {
739 self.to_release = Some((last.clone(), false));
741 false
742 } else {
743 panic!("No input and no last released item to re-release");
744 }
745 } else if !force_trigger
746 && let Some(last) = &self.last_released
747 && produce().generate(driver).unwrap()
748 {
749 self.to_release = Some((last.clone(), false));
751 false
752 } else {
753 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
755 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
757 self.to_release = Some((item, true));
758 true
759 }
760 }
761}
762
763pub struct OptionalInitNoneHook<T> {
773 input: Rc<RefCell<VecDeque<T>>>,
774 to_release: Option<(Option<T>, bool)>, last_released: Option<T>, skipped_states: Vec<T>,
777 output: Sender<T>,
778 batch_location: HookLocationMeta,
779 format_item_debug: fn(&T) -> Option<String>,
780}
781
782impl<T: Clone> OptionalInitNoneHook<T> {
783 pub fn new(
784 input: Rc<RefCell<VecDeque<T>>>,
785 output: Sender<T>,
786 batch_location: HookLocationMeta,
787 format_item_debug: fn(&T) -> Option<String>,
788 ) -> Self {
789 Self {
790 input,
791 to_release: None,
792 last_released: None,
793 skipped_states: vec![],
794 output,
795 batch_location,
796 format_item_debug,
797 }
798 }
799}
800
801impl<T: Clone> RuntimeHook for OptionalInitNoneHook<T> {
802 fn has_pending_input(&self) -> bool {
803 !self.input.borrow().is_empty()
804 }
805
806 fn only_one_possible_decision(&self) -> bool {
807 self.input.borrow().is_empty()
811 }
812
813 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
814 let Some((to_release, is_new)) = self.to_release.take() else {
815 panic!("No decision to release");
816 };
817
818 if let Some(value) = &to_release {
819 self.last_released = Some(value.clone());
820 }
821
822 if let Some(log_writer) = log_writer {
823 let HookLocationMeta {
824 location: batch_location,
825 line,
826 caret_indent,
827 } = self.batch_location;
828 let note_str = match (&to_release, is_new) {
829 (None, _) => "^ releasing null snapshot".to_owned(),
830 (Some(value), true) => {
831 if self.skipped_states.is_empty() {
832 format!(
833 "^ releasing snapshot: {:?}",
834 ManualDebug(value, self.format_item_debug)
835 )
836 } else {
837 format!(
838 "^ releasing snapshot: {:?} (skipping earlier states: {:?})",
839 ManualDebug(value, self.format_item_debug),
840 self.skipped_states
841 .iter()
842 .map(|s| ManualDebug(s, self.format_item_debug))
843 .collect::<Vec<_>>()
844 )
845 }
846 }
847 (Some(value), false) => format!(
848 "^ releasing unchanged snapshot: {:?}",
849 ManualDebug(value, self.format_item_debug)
850 ),
851 };
852
853 log_release(
854 log_writer,
855 batch_location,
856 line,
857 caret_indent,
858 ¬e_str,
859 colored::Color::Green,
860 );
861 }
862
863 if let Some(value) = to_release {
864 self.output.try_send(value).unwrap();
865 }
866 }
867
868 fn location_meta(&self) -> HookLocationMeta {
869 self.batch_location
870 }
871}
872
873impl<T: Clone> TickInputHook for OptionalInitNoneHook<T> {
874 fn can_trigger_tick(&self) -> bool {
875 !self.input.borrow().is_empty()
879 }
880
881 fn autonomous_decision<'a>(
882 &mut self,
883 driver: &mut Borrowed<'a>,
884 force_nontrivial: bool,
885 ) -> bool {
886 let mut current_input = self.input.borrow_mut();
887 if current_input.is_empty() {
888 if force_nontrivial {
890 panic!("Cannot make nontrivial decision when there is no input");
891 }
892
893 if let Some(last) = &self.last_released {
894 self.to_release = Some((Some(last.clone()), false));
896 } else {
897 self.to_release = Some((None, false));
899 }
900 false
901 } else if !force_nontrivial && produce().generate(driver).unwrap() {
902 if let Some(last) = &self.last_released {
904 self.to_release = Some((Some(last.clone()), false));
906 } else {
907 self.to_release = Some((None, false));
910 }
911 false
912 } else {
913 let idx_to_release = (0..current_input.len()).generate(driver).unwrap();
915 self.skipped_states = current_input.drain(0..idx_to_release).collect(); let item = current_input.pop_front().unwrap();
917 self.to_release = Some((Some(item), true));
918 true
919 }
920 }
921}
922
923#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
926pub enum SnapshotDecision<T> {
927 Reveal(T),
930 RevealNext,
932 RevealLatest,
934 Keep,
936}
937
938impl<T> ScriptDecision for SnapshotDecision<T>
939where
940 T: serde::Serialize + serde::de::DeserializeOwned,
941{
942 fn describe(&self) -> String {
943 match self {
944 SnapshotDecision::Reveal(_) => "reveal(..)".to_owned(),
945 SnapshotDecision::RevealNext => "reveal_next()".to_owned(),
946 SnapshotDecision::RevealLatest => "reveal_latest()".to_owned(),
947 SnapshotDecision::Keep => "keep()".to_owned(),
948 }
949 }
950}
951
952#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
955pub struct SnapshotStatus {
956 pub newer_versions: usize,
958}
959
960impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableHook
961 for SingletonHook<T>
962{
963 type Decision = SnapshotDecision<T>;
964 type Status = SnapshotStatus;
965
966 fn is_honorable(&self, decision: &SnapshotDecision<T>) -> Result<bool, String> {
967 let input = self.input.borrow();
968 Ok(match decision {
969 SnapshotDecision::Reveal(target) => input.iter().any(|version| version == target),
970 SnapshotDecision::RevealNext => !input.is_empty(),
971 SnapshotDecision::RevealLatest => !input.is_empty() || self.last_released.is_some(),
972 SnapshotDecision::Keep => self.last_released.is_some(),
973 })
974 }
975
976 fn apply(&mut self, decision: SnapshotDecision<T>) {
977 match decision {
978 SnapshotDecision::Reveal(target) => {
979 let mut input = self.input.borrow_mut();
980 let idx = input.iter().position(|version| *version == target).unwrap();
981 self.skipped_states = input.drain(0..idx).collect();
982 let item = input.pop_front().unwrap();
983 self.to_release = Some((item, true));
984 }
985 SnapshotDecision::RevealNext => {
986 let mut input = self.input.borrow_mut();
987 self.skipped_states = vec![];
988 let item = input.pop_front().unwrap();
989 self.to_release = Some((item, true));
990 }
991 SnapshotDecision::RevealLatest => {
992 let mut input = self.input.borrow_mut();
993 if input.is_empty() {
994 self.skipped_states = vec![];
995 self.to_release = Some((self.last_released.clone().unwrap(), false));
996 } else {
997 let skip_count = input.len() - 1;
998 self.skipped_states = input.drain(0..skip_count).collect();
999 let item = input.pop_front().unwrap();
1000 self.to_release = Some((item, true));
1001 }
1002 }
1003 SnapshotDecision::Keep => {
1004 self.skipped_states = vec![];
1005 self.to_release = Some((self.last_released.clone().unwrap(), false));
1006 }
1007 }
1008 }
1009
1010 fn implicit(&mut self) {
1011 if let Some(last) = &self.last_released {
1012 self.skipped_states = vec![];
1013 self.to_release = Some((last.clone(), false));
1014 } else {
1015 abort!("scripted snapshot hook asked for implicit behavior with no revealed value");
1018 }
1019 }
1020
1021 fn status(&self) -> SnapshotStatus {
1022 SnapshotStatus {
1023 newer_versions: self.input.borrow().len(),
1024 }
1025 }
1026
1027 fn describe_pending(&self) -> Option<String> {
1028 let input = self.input.borrow();
1029 (!input.is_empty()).then(|| {
1030 format!(
1031 "{} buffered version(s): {:?}",
1032 input.len(),
1033 TruncatedVecDebug(RefCell::new(Some(input.iter())), 8, self.format_item_debug)
1034 )
1035 })
1036 }
1037}
1038
1039impl<T: Clone + PartialEq + serde::Serialize + serde::de::DeserializeOwned> ScriptableTickInputHook
1040 for SingletonHook<T>
1041{
1042 fn decision_triggers_tick(&self, decision: &SnapshotDecision<T>) -> bool {
1043 match decision {
1044 SnapshotDecision::Reveal(_) | SnapshotDecision::RevealNext => true,
1045 SnapshotDecision::RevealLatest => !self.input.borrow().is_empty(),
1046 SnapshotDecision::Keep => false,
1047 }
1048 }
1049}
1050pub struct PassthroughSingletonHook<T> {
1055 input: Rc<RefCell<VecDeque<T>>>,
1056 to_release: Option<T>,
1057 output: Sender<T>,
1058 batch_location: HookLocationMeta,
1059 format_item_debug: fn(&T) -> Option<String>,
1060}
1061
1062impl<T> PassthroughSingletonHook<T> {
1063 pub fn new(
1064 input: Rc<RefCell<VecDeque<T>>>,
1065 output: Sender<T>,
1066 batch_location: HookLocationMeta,
1067 format_item_debug: fn(&T) -> Option<String>,
1068 ) -> Self {
1069 Self {
1070 input,
1071 to_release: None,
1072 output,
1073 batch_location,
1074 format_item_debug,
1075 }
1076 }
1077}
1078
1079impl<T> RuntimeHook for PassthroughSingletonHook<T> {
1080 fn has_pending_input(&self) -> bool {
1081 !self.input.borrow().is_empty()
1082 }
1083
1084 fn only_one_possible_decision(&self) -> bool {
1085 true
1090 }
1091
1092 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1093 if let Some(to_release) = self.to_release.take() {
1094 if let Some(log_writer) = log_writer {
1095 let HookLocationMeta {
1096 location: batch_location,
1097 line,
1098 caret_indent,
1099 } = self.batch_location;
1100 let note_str = format!(
1101 "^ releasing snapshot: {:?}",
1102 ManualDebug(&to_release, self.format_item_debug)
1103 );
1104
1105 log_release(
1106 log_writer,
1107 batch_location,
1108 line,
1109 caret_indent,
1110 ¬e_str,
1111 colored::Color::Green,
1112 );
1113 }
1114
1115 self.output.try_send(to_release).unwrap();
1116 } else {
1117 panic!("No decision to release");
1118 }
1119 }
1120
1121 fn location_meta(&self) -> HookLocationMeta {
1122 self.batch_location
1123 }
1124}
1125
1126impl<T> TickInputHook for PassthroughSingletonHook<T> {
1127 fn can_trigger_tick(&self) -> bool {
1128 !self.input.borrow().is_empty()
1129 }
1130
1131 fn autonomous_decision<'a>(
1132 &mut self,
1133 _driver: &mut Borrowed<'a>,
1134 _force_trigger: bool,
1135 ) -> bool {
1136 let mut current_input = self.input.borrow_mut();
1137 if let Some(item) = current_input.pop_back() {
1139 current_input.clear();
1140 self.to_release = Some(item);
1141 true
1142 } else {
1143 false
1144 }
1145 }
1146}
1147
1148pub struct KeyedSingletonHook<K: Hash + Eq + Clone, V: Clone> {
1149 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>, to_release: Option<Vec<(K, V, bool)>>, last_released: FxHashMap<K, V>,
1152 skipped_states: FxHashMap<K, Vec<V>>,
1153 output: Sender<(K, V)>,
1154 batch_location: HookLocationMeta,
1155 format_key_debug: fn(&K) -> Option<String>,
1156 format_value_debug: fn(&V) -> Option<String>,
1157}
1158
1159impl<K: Hash + Eq + Clone, V: Clone> KeyedSingletonHook<K, V> {
1160 pub fn new(
1161 input: Rc<RefCell<FxHashMap<K, VecDeque<V>>>>,
1162 output: Sender<(K, V)>,
1163 batch_location: HookLocationMeta,
1164 format_key_debug: fn(&K) -> Option<String>,
1165 format_value_debug: fn(&V) -> Option<String>,
1166 ) -> Self {
1167 Self {
1168 input,
1169 to_release: None,
1170 last_released: FxHashMap::default(),
1171 skipped_states: FxHashMap::default(),
1172 output,
1173 batch_location,
1174 format_key_debug,
1175 format_value_debug,
1176 }
1177 }
1178}
1179
1180impl<K: Hash + Eq + Clone, V: Clone> RuntimeHook for KeyedSingletonHook<K, V> {
1181 fn has_pending_input(&self) -> bool {
1182 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1183 !self.input.borrow().values().all(|q| q.is_empty())
1184 }
1185
1186 fn only_one_possible_decision(&self) -> bool {
1187 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1190 self.input.borrow().values().all(|q| q.is_empty())
1191 }
1192
1193 fn release_decision(&mut self, log_writer: Option<&mut dyn std::fmt::Write>) {
1194 if let Some(to_release) = self.to_release.take() {
1195 if let Some(log_writer) = log_writer {
1196 let HookLocationMeta {
1197 location: batch_location,
1198 line,
1199 caret_indent,
1200 } = self.batch_location;
1201 let note_str = if to_release.is_empty() {
1202 "^ releasing no items".to_owned()
1203 } else {
1204 let mut mapping_text = String::new();
1205 for (key, value, is_new) in &to_release {
1206 let entry_text = if *is_new {
1207 format!(
1208 "{:?}: {:?}",
1209 ManualDebug(key, self.format_key_debug),
1210 ManualDebug(value, self.format_value_debug)
1211 )
1212 } else {
1213 format!(
1214 "{:?}: {:?} (unchanged)",
1215 ManualDebug(key, self.format_key_debug),
1216 ManualDebug(value, self.format_value_debug)
1217 )
1218 };
1219 if !mapping_text.is_empty() {
1220 mapping_text.push_str(", ");
1221 }
1222 mapping_text.push_str(&entry_text);
1223 }
1224 format!("^ releasing items: {{ {} }}", mapping_text)
1225 };
1226
1227 log_release(
1228 log_writer,
1229 batch_location,
1230 line,
1231 caret_indent,
1232 ¬e_str,
1233 colored::Color::Green,
1234 );
1235 }
1236
1237 for (key, value, _) in to_release {
1238 self.output.try_send((key, value)).unwrap();
1239 }
1240 } else {
1241 panic!("No decision to release");
1242 }
1243 }
1244
1245 fn location_meta(&self) -> HookLocationMeta {
1246 self.batch_location
1247 }
1248}
1249
1250impl<K: Hash + Eq + Clone, V: Clone> TickInputHook for KeyedSingletonHook<K, V> {
1251 fn can_trigger_tick(&self) -> bool {
1252 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1253 !self.input.borrow().values().all(|q| q.is_empty())
1254 }
1255
1256 fn autonomous_decision<'a>(
1257 &mut self,
1258 driver: &mut Borrowed<'a>,
1259 mut force_trigger: bool,
1260 ) -> bool {
1261 let mut current_input = self.input.borrow_mut();
1262 self.to_release = Some(vec![]);
1263 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1264 let nonempty_key_count = current_input.values().filter(|q| !q.is_empty()).count();
1265
1266 let mut remaining_nonempty_keys = nonempty_key_count;
1267 let mut any_triggered = false;
1268 #[expect(clippy::disallowed_methods, reason = "FxHasher is deterministic")]
1269 for (key, queue) in current_input.iter_mut() {
1270 if queue.is_empty() {
1271 self.to_release.as_mut().unwrap().push((
1272 key.clone(),
1273 self.last_released.get(key).unwrap().clone(),
1274 false,
1275 ));
1276
1277 continue;
1278 }
1279
1280 remaining_nonempty_keys -= 1;
1281
1282 let must_reveal = force_trigger && remaining_nonempty_keys == 0;
1283
1284 if !must_reveal
1285 && self.last_released.contains_key(key)
1286 && produce().generate(driver).unwrap()
1287 {
1288 let last = self.last_released.get(key).unwrap().clone();
1290 self.to_release
1291 .as_mut()
1292 .unwrap()
1293 .push((key.clone(), last, false));
1294 } else {
1295 let allow_null_release = !must_reveal && !self.last_released.contains_key(key);
1296 if allow_null_release && produce().generate(driver).unwrap() {
1297 continue;
1299 } else {
1300 let idx_to_release = (0..queue.len()).generate(driver).unwrap();
1302 let skipped: Vec<V> = queue.drain(0..idx_to_release).collect();
1303 let item = queue.pop_front().unwrap();
1304 self.skipped_states.insert(key.clone(), skipped);
1305 self.to_release
1306 .as_mut()
1307 .unwrap()
1308 .push((key.clone(), item.clone(), true));
1309 self.last_released.insert(key.clone(), item);
1310
1311 any_triggered |= true;
1312 force_trigger = false;
1313 }
1314 }
1315 }
1316
1317 any_triggered
1318 }
1319}