1#![allow(clippy::too_many_arguments)]
8mod analysis_methods;
9#[cfg(test)]
10pub mod tests;
11
12use crate::fixed_point_graph::FixedPointAnalysisKind;
13use crate::fixed_point_graph::FixedPointForwardGraph;
14use std::collections::HashSet;
15
16pub use crate::fixed_point_resident_batch::{
17 FixedPointResidentBatch, FixedPointResidentBatchStats,
18};
19pub use crate::fixed_point_resident_cache::FixedPointResidentGraphCache;
20pub use crate::fixed_point_resident_frontier::FixedPointResidentFrontierScratch;
21pub use crate::fixed_point_resident_plan::{
22 FixedPointBorrowedResidentPlan, FixedPointResidentPlan,
23};
24
25#[derive(Clone, Debug, PartialEq)]
27pub struct FixedPointResidentGraph {
28 kind: FixedPointAnalysisKind,
29 node_count: u32,
30 edge_count: u32,
31 layout_hash: u64,
32 resources: Vec<vyre::backend::Resource>,
33}
34
35impl FixedPointResidentGraph {
36 pub fn upload(
38 backend: &dyn vyre::VyreBackend,
39 graph: &FixedPointForwardGraph,
40 ) -> Result<Self, vyre::BackendError> {
41 let buffers = [
42 graph.pg_nodes_bytes.as_slice(),
43 graph.edge_offsets_bytes.as_slice(),
44 graph.edge_targets_bytes.as_slice(),
45 graph.edge_kind_mask_bytes.as_slice(),
46 ];
47 let mut resources = crate::staging_reserve::reserved_vec(
48 buffers.len() + 1,
49 "fixed-point resident graph resource",
50 )
51 .map_err(vyre::BackendError::new)?;
52 for bytes in buffers {
53 let resource = match backend.allocate_resident(bytes.len()) {
54 Ok(resource) => resource,
55 Err(error) => {
56 let _ = free_uploaded_prefix(backend, resources);
57 return Err(error);
58 }
59 };
60 resources.push(resource);
61 }
62 let uploads = [
63 (&resources[0], buffers[0]),
64 (&resources[1], buffers[1]),
65 (&resources[2], buffers[2]),
66 (&resources[3], buffers[3]),
67 ];
68 if let Err(error) = backend.upload_resident_many(&uploads) {
69 let _ = free_uploaded_prefix(backend, resources);
70 return Err(error);
71 }
72 resources.push(resources[0].clone());
73 Ok(Self {
74 kind: graph.kind(),
75 node_count: graph.node_count(),
76 edge_count: graph.edge_count(),
77 layout_hash: graph.stable_layout_hash(),
78 resources,
79 })
80 }
81
82 #[must_use]
84 pub fn node_count(&self) -> u32 {
85 self.node_count
86 }
87
88 #[must_use]
90 pub fn kind(&self) -> FixedPointAnalysisKind {
91 self.kind
92 }
93
94 pub fn require_kind(
97 &self,
98 expected: FixedPointAnalysisKind,
99 consumer: &str,
100 ) -> Result<(), String> {
101 if self.kind == expected {
102 return Ok(());
103 }
104 Err(resident_kind_mismatch(consumer, self.kind, expected))
105 }
106
107 #[must_use]
109 pub fn edge_count(&self) -> u32 {
110 self.edge_count
111 }
112
113 #[must_use]
115 pub fn stable_layout_hash(&self) -> u64 {
116 self.layout_hash
117 }
118
119 pub fn require_same_layout(
121 &self,
122 graph: &FixedPointForwardGraph,
123 consumer: &str,
124 ) -> Result<(), String> {
125 if self.kind != graph.kind() {
126 return Err(resident_prepared_kind_mismatch(
127 consumer,
128 self.kind,
129 graph.kind(),
130 ));
131 }
132 if self.layout_hash != graph.stable_layout_hash() {
133 let mut scratch = crate::error_format::ErrorFormatScratch::default();
134 return Err(crate::error_format::write_fixed_point_layout_mismatch(
135 &mut scratch,
136 consumer,
137 self.layout_hash,
138 graph.stable_layout_hash(),
139 ));
140 }
141 Ok(())
142 }
143
144 #[must_use]
146 pub fn resident_resource_count(&self) -> usize {
147 self.resources.len()
148 }
149
150 pub fn resources_with_frontier_into(
152 &self,
153 frontier_bytes: &[u8],
154 resources: &mut Vec<vyre::backend::Resource>,
155 ) -> Result<(), String> {
156 let invariant_len = stage_frontier_resources(&self.resources, resources)?;
157 rewrite_borrowed_resource(&mut resources[invariant_len], frontier_bytes)?;
158 rewrite_borrowed_resource(&mut resources[invariant_len + 1], frontier_bytes)?;
159 Ok(())
160 }
161
162 pub fn resources_with_resident_frontier_into(
164 &self,
165 frontier_in: &vyre::backend::Resource,
166 frontier_out: &vyre::backend::Resource,
167 resources: &mut Vec<vyre::backend::Resource>,
168 ) -> Result<(), String> {
169 let invariant_len = self.resources.len();
170 let total_len = invariant_len.checked_add(2).ok_or_else(|| {
171 "fixed-point resident resource list length overflowed usize. Fix: shard resident resources before dispatch."
172 .to_string()
173 })?;
174 crate::staging_reserve::reserve_vec(
175 resources,
176 total_len,
177 "fixed-point resident resource scratch",
178 )?;
179 if resources.len() < total_len {
180 resources.resize_with(total_len, vyre::backend::Resource::default);
181 } else {
182 resources.truncate(total_len);
183 }
184 for (slot, resource) in resources[..invariant_len].iter_mut().zip(&self.resources) {
185 match resource {
186 vyre::backend::Resource::Resident(id) => {
187 if !matches!(slot, vyre::backend::Resource::Resident(existing) if *existing == *id)
188 {
189 *slot = vyre::backend::Resource::Resident(*id);
190 }
191 }
192 vyre::backend::Resource::Borrowed(_) => {
193 if slot != resource {
194 *slot = resource.clone();
195 }
196 }
197 }
198 }
199 resources[invariant_len] = frontier_in.clone();
200 resources[invariant_len + 1] = frontier_out.clone();
201 Ok(())
202 }
203
204 pub fn resources_with_resident_frontier_changed_into(
207 &self,
208 frontier: &vyre::backend::Resource,
209 changed: &vyre::backend::Resource,
210 resources: &mut Vec<vyre::backend::Resource>,
211 ) -> Result<(), String> {
212 let invariant_len = stage_frontier_resources(&self.resources, resources)?;
213 if resources[invariant_len] != *frontier {
214 resources[invariant_len] = frontier.clone();
215 }
216 if resources[invariant_len + 1] != *changed {
217 resources[invariant_len + 1] = changed.clone();
218 }
219 Ok(())
220 }
221
222 pub fn free(self, backend: &dyn vyre::VyreBackend) -> Result<(), vyre::BackendError> {
224 let mut first_error = None;
225 let mut freed = HashSet::new();
226 freed.try_reserve(self.resources.len()).map_err(|source| {
227 vyre::BackendError::new(format!(
228 "fixed-point resident graph free could not reserve {} released handle slot(s): {source}. Fix: shard resident graph resources before cleanup.",
229 self.resources.len()
230 ))
231 })?;
232 for resource in self.resources {
233 if let vyre::backend::Resource::Resident(id) = resource {
234 if !freed.insert(id) {
235 continue;
236 }
237 if let Err(error) = backend.free_resident(vyre::backend::Resource::Resident(id)) {
238 first_error.get_or_insert(error);
239 }
240 } else if let Err(error) = backend.free_resident(resource) {
241 first_error.get_or_insert(error);
242 }
243 }
244 if let Some(error) = first_error {
245 Err(error)
246 } else {
247 Ok(())
248 }
249 }
250}
251
252fn stage_frontier_resources(
253 invariant_resources: &[vyre::backend::Resource],
254 resources: &mut Vec<vyre::backend::Resource>,
255) -> Result<usize, String> {
256 let invariant_len = invariant_resources.len();
257 let total_len = invariant_len.checked_add(2).ok_or_else(|| {
258 "fixed-point resident resource list length overflowed usize. Fix: shard resident resources before dispatch."
259 .to_string()
260 })?;
261 crate::staging_reserve::reserve_vec(
262 resources,
263 total_len,
264 "fixed-point resident resource scratch",
265 )?;
266 if resources.len() < total_len {
267 resources.resize_with(total_len, vyre::backend::Resource::default);
268 } else {
269 resources.truncate(total_len);
270 }
271 for (slot, resource) in resources[..invariant_len]
272 .iter_mut()
273 .zip(invariant_resources)
274 {
275 match resource {
276 vyre::backend::Resource::Resident(id) => {
277 if !matches!(slot, vyre::backend::Resource::Resident(existing) if *existing == *id)
278 {
279 *slot = vyre::backend::Resource::Resident(*id);
280 }
281 }
282 vyre::backend::Resource::Borrowed(_) => {
283 if slot != resource {
284 *slot = resource.clone();
285 }
286 }
287 }
288 }
289 Ok(invariant_len)
290}
291
292fn rewrite_borrowed_resource(
293 resource: &mut vyre::backend::Resource,
294 bytes: &[u8],
295) -> Result<(), String> {
296 match resource {
297 vyre::backend::Resource::Borrowed(buffer) => {
298 if buffer.len() == bytes.len() {
299 buffer.copy_from_slice(bytes);
300 } else {
301 buffer.clear();
302 crate::staging_reserve::reserve_vec(
303 buffer,
304 bytes.len(),
305 "fixed-point borrowed frontier bytes",
306 )?;
307 buffer.extend_from_slice(bytes);
308 }
309 }
310 vyre::backend::Resource::Resident(_) => {
311 let mut buffer = crate::staging_reserve::reserved_vec(
312 bytes.len(),
313 "fixed-point borrowed frontier bytes",
314 )?;
315 buffer.extend_from_slice(bytes);
316 *resource = vyre::backend::Resource::Borrowed(buffer);
317 }
318 }
319 Ok(())
320}
321
322#[derive(Clone, Copy)]
324pub(crate) struct FixedPointResidentLoopLabels {
325 pub(crate) analysis: &'static str,
326 pub(crate) output: &'static str,
327 pub(crate) tail: &'static str,
328}
329
330#[derive(Clone, Copy)]
332pub(crate) struct FixedPointResidentClosureLabels {
333 pub(crate) kind: FixedPointAnalysisKind,
334 pub(crate) analysis: &'static str,
335 pub(crate) capacity: &'static str,
336 pub(crate) output: &'static str,
337 pub(crate) tail: &'static str,
338}
339
340impl FixedPointResidentClosureLabels {
341 fn loop_labels(self) -> FixedPointResidentLoopLabels {
342 FixedPointResidentLoopLabels {
343 analysis: self.analysis,
344 output: self.output,
345 tail: self.tail,
346 }
347 }
348}
349
350pub(crate) fn reserved_resident_closure_result(
353 stage: &str,
354 graph: &FixedPointResidentGraph,
355) -> Result<Vec<u32>, String> {
356 crate::staging_reserve::reserved_fixed_point_result(stage, graph.node_count())
357}
358
359pub(crate) fn run_borrowed_resident_closure_into(
362 pipeline: &dyn vyre::CompiledPipeline,
363 graph: &FixedPointResidentGraph,
364 seed_bits: &[u32],
365 max_iterations: u32,
366 config: &vyre::DispatchConfig,
367 scratch: &mut crate::fixed_point_scratch::FixedPointScratch,
368 result: &mut Vec<u32>,
369 labels: FixedPointResidentClosureLabels,
370 kind_consumer: &str,
371) -> Result<(), String> {
372 let words = prepare_resident_closure_frontier(
373 graph,
374 seed_bits,
375 max_iterations,
376 scratch,
377 labels,
378 kind_consumer,
379 )?;
380 scratch.begin_frontier_density(graph.node_count());
381 scratch.prepare_grid_dispatch_config(config, [graph.node_count().max(1), 1, 1]);
382 scratch.prepare_next_words(words)?;
383 for _ in 0..max_iterations {
384 crate::dispatch_decode::try_pack_u32_into(&scratch.frontier_words, &mut scratch.frontier_bytes, "weir u32 byte staging")?;
385 graph.resources_with_frontier_into(&scratch.frontier_bytes, &mut scratch.resources)?;
386 pipeline
387 .dispatch_persistent_handles_into(
388 &scratch.resources,
389 &scratch.dispatch_config,
390 &mut scratch.outputs,
391 )
392 .map_err(|error| error.to_string())?;
393 crate::dispatch_decode::unpack_only_exact_u32_into(
394 &scratch.outputs,
395 labels.analysis,
396 labels.output,
397 words,
398 &mut scratch.next,
399 )?;
400 crate::dispatch_decode::require_bitset_tail_clear(
401 labels.tail,
402 &scratch.next,
403 graph.node_count(),
404 )?;
405 scratch.record_decoded_frontier_transition();
406 if crate::fixed_point_scratch::copy_if_converged(
407 &scratch.next,
408 &scratch.frontier_words,
409 result,
410 )? {
411 return Ok(());
412 }
413 std::mem::swap(&mut scratch.frontier_words, &mut scratch.next);
414 }
415 Err(format!(
416 "{} did not converge within {max_iterations} iterations",
417 labels.analysis
418 ))
419}
420
421pub(crate) fn run_ephemeral_resident_closure_into(
424 backend: &dyn vyre::VyreBackend,
425 pipeline: &dyn vyre::CompiledPipeline,
426 graph: &FixedPointResidentGraph,
427 seed_bits: &[u32],
428 max_iterations: u32,
429 config: &vyre::DispatchConfig,
430 scratch: &mut crate::fixed_point_scratch::FixedPointScratch,
431 result: &mut Vec<u32>,
432 labels: FixedPointResidentClosureLabels,
433 kind_consumer: &str,
434) -> Result<(), String> {
435 let words = prepare_resident_closure_frontier(
436 graph,
437 seed_bits,
438 max_iterations,
439 scratch,
440 labels,
441 kind_consumer,
442 )?;
443 let frontier_byte_len = resident_frontier_byte_len(labels, words)?;
444 with_ephemeral_resident_frontier_pair(
445 backend,
446 frontier_byte_len,
447 |frontier_in, frontier_out| {
448 run_resident_frontier_pair_loop(
449 backend,
450 pipeline,
451 graph,
452 frontier_in,
453 frontier_out,
454 max_iterations,
455 config,
456 scratch,
457 result,
458 labels.loop_labels(),
459 )
460 },
461 )
462}
463
464pub(crate) fn run_reusable_resident_closure_into(
467 backend: &dyn vyre::VyreBackend,
468 pipeline: &dyn vyre::CompiledPipeline,
469 graph: &FixedPointResidentGraph,
470 seed_bits: &[u32],
471 max_iterations: u32,
472 config: &vyre::DispatchConfig,
473 scratch: &mut crate::fixed_point_scratch::FixedPointScratch,
474 resident_frontier: &mut crate::fixed_point_resident::FixedPointResidentFrontierScratch,
475 result: &mut Vec<u32>,
476 labels: FixedPointResidentClosureLabels,
477 kind_consumer: &str,
478) -> Result<(), String> {
479 let words = prepare_resident_closure_frontier(
480 graph,
481 seed_bits,
482 max_iterations,
483 scratch,
484 labels,
485 kind_consumer,
486 )?;
487 let frontier_byte_len = resident_frontier_byte_len(labels, words)?;
488 let (frontier_in, frontier_out) =
489 resident_frontier.ensure_pair_refs(backend, frontier_byte_len)?;
490 run_resident_frontier_pair_loop(
491 backend,
492 pipeline,
493 graph,
494 frontier_in,
495 frontier_out,
496 max_iterations,
497 config,
498 scratch,
499 result,
500 labels.loop_labels(),
501 )
502}
503
504fn prepare_resident_closure_frontier(
505 graph: &FixedPointResidentGraph,
506 seed_bits: &[u32],
507 max_iterations: u32,
508 scratch: &mut crate::fixed_point_scratch::FixedPointScratch,
509 labels: FixedPointResidentClosureLabels,
510 kind_consumer: &str,
511) -> Result<usize, String> {
512 graph.require_kind(labels.kind, kind_consumer)?;
513 crate::dispatch_decode::require_positive_iterations(labels.analysis, max_iterations)?;
514 let words = crate::dispatch_decode::bitset_word_capacity(labels.capacity, graph.node_count())?;
515 crate::dispatch_decode::require_bitset_words(labels.analysis, seed_bits, words)?;
516 crate::dispatch_decode::require_bitset_tail_clear(
517 labels.analysis,
518 seed_bits,
519 graph.node_count(),
520 )?;
521 scratch.prepare_frontier_words(words, seed_bits)?;
522 Ok(words)
523}
524
525fn resident_frontier_byte_len(
526 labels: FixedPointResidentClosureLabels,
527 words: usize,
528) -> Result<usize, String> {
529 words.checked_mul(4).ok_or_else(|| {
530 format!(
531 "{} resident frontier byte length overflow. Fix: shard the graph before GPU dispatch.",
532 labels.analysis
533 )
534 })
535}
536
537pub(crate) fn with_ephemeral_resident_frontier_pair<F>(
540 backend: &dyn vyre::VyreBackend,
541 byte_len: usize,
542 run: F,
543) -> Result<(), String>
544where
545 F: FnOnce(&vyre::backend::Resource, &vyre::backend::Resource) -> Result<(), String>,
546{
547 let frontier_in = backend
548 .allocate_resident(byte_len)
549 .map_err(|error| error.to_string())?;
550 let frontier_out = match backend.allocate_resident(byte_len) {
551 Ok(resource) => resource,
552 Err(error) => {
553 let _ = backend.free_resident(frontier_in);
554 return Err(error.to_string());
555 }
556 };
557 let run_result = run(&frontier_in, &frontier_out);
558 let free_in = backend
559 .free_resident(frontier_in)
560 .map_err(|error| error.to_string());
561 let free_out = backend
562 .free_resident(frontier_out)
563 .map_err(|error| error.to_string());
564 match (run_result, free_in, free_out) {
565 (Err(error), _, _) => Err(error),
566 (Ok(_), Err(error), _) | (Ok(_), _, Err(error)) => Err(error),
567 (Ok(()), Ok(()), Ok(())) => Ok(()),
568 }
569}
570
571pub(crate) fn run_resident_frontier_pair_loop(
573 backend: &dyn vyre::VyreBackend,
574 pipeline: &dyn vyre::CompiledPipeline,
575 graph: &FixedPointResidentGraph,
576 frontier_in: &vyre::backend::Resource,
577 frontier_out: &vyre::backend::Resource,
578 max_iterations: u32,
579 config: &vyre::DispatchConfig,
580 scratch: &mut crate::fixed_point_scratch::FixedPointScratch,
581 result: &mut Vec<u32>,
582 labels: FixedPointResidentLoopLabels,
583) -> Result<(), String> {
584 let words = scratch.frontier_words.len();
585 scratch.begin_frontier_density(graph.node_count());
586 scratch.prepare_grid_dispatch_config(config, [graph.node_count().max(1), 1, 1]);
587 scratch.prepare_next_words(words)?;
588 crate::dispatch_decode::try_pack_u32_into(&scratch.frontier_words, &mut scratch.frontier_bytes, "weir u32 byte staging")?;
589 backend
590 .upload_resident_many(&[
591 (frontier_in, scratch.frontier_bytes.as_slice()),
592 (frontier_out, scratch.frontier_bytes.as_slice()),
593 ])
594 .map_err(|error| error.to_string())?;
595 let mut current_frontier = frontier_in;
596 let mut next_frontier = frontier_out;
597 for _ in 0..max_iterations {
598 graph.resources_with_resident_frontier_into(
599 current_frontier,
600 next_frontier,
601 &mut scratch.resources,
602 )?;
603 pipeline
604 .dispatch_persistent_handles_into(
605 &scratch.resources,
606 &scratch.dispatch_config,
607 &mut scratch.outputs,
608 )
609 .map_err(|error| error.to_string())?;
610 crate::dispatch_decode::unpack_only_exact_u32_into(
611 &scratch.outputs,
612 labels.analysis,
613 labels.output,
614 words,
615 &mut scratch.next,
616 )?;
617 crate::dispatch_decode::require_bitset_tail_clear(
618 labels.tail,
619 &scratch.next,
620 graph.node_count(),
621 )?;
622 scratch.record_decoded_frontier_transition();
623 let supports_gpu_equal = backend
624 .supported_ops()
625 .contains(vyre_primitives::bitset::equal::OP_ID);
626 let converged = if supports_gpu_equal {
627 crate::fixed_point_scratch::converged_via_gpu_backend(
628 &scratch.outputs[0],
629 &scratch.frontier_bytes,
630 words,
631 backend,
632 )
633 .unwrap_or(false)
634 } else {
635 crate::fixed_point_scratch::copy_if_converged(
636 &scratch.next,
637 &scratch.frontier_words,
638 result,
639 )?
640 };
641 if converged {
642 if !supports_gpu_equal {
643 } else {
645 crate::fixed_point_scratch::copy_words_into(&scratch.next, result)?;
646 }
647 return Ok(());
648 }
649 std::mem::swap(&mut scratch.frontier_words, &mut scratch.next);
650 crate::dispatch_decode::try_pack_u32_into(&scratch.frontier_words, &mut scratch.frontier_bytes, "weir u32 byte staging")?;
651 std::mem::swap(&mut current_frontier, &mut next_frontier);
652 }
653 Err(format!(
654 "{} did not converge within {max_iterations} iterations",
655 labels.analysis
656 ))
657}
658
659fn free_uploaded_prefix(
660 backend: &dyn vyre::VyreBackend,
661 resources: Vec<vyre::backend::Resource>,
662) -> Result<(), vyre::BackendError> {
663 let mut freed = HashSet::new();
664 freed.try_reserve(resources.len()).map_err(|source| {
665 vyre::BackendError::new(format!(
666 "fixed-point resident upload rollback could not reserve {} released handle slot(s): {source}. Fix: shard resident graph upload before cleanup.",
667 resources.len()
668 ))
669 })?;
670 for resource in resources {
671 if let vyre::backend::Resource::Resident(id) = resource {
672 if !freed.insert(id) {
673 continue;
674 }
675 let _ = backend.free_resident(vyre::backend::Resource::Resident(id));
676 } else {
677 let _ = backend.free_resident(resource);
678 }
679 }
680 Ok(())
681}
682
683#[cold]
684fn resident_kind_mismatch(
685 consumer: &str,
686 got: FixedPointAnalysisKind,
687 expected: FixedPointAnalysisKind,
688) -> String {
689 let mut scratch = crate::error_format::ErrorFormatScratch::default();
690 let _ = std::fmt::Write::write_fmt(
691 &mut scratch.buf,
692 format_args!(
693 "{consumer} received a {got:?} resident fixed-point graph, expected {expected:?}. Fix: upload the graph with the matching Weir analysis constructor before resident dispatch."
694 ),
695 );
696 scratch.finish()
697}
698
699#[cold]
700fn resident_prepared_kind_mismatch(
701 consumer: &str,
702 resident: FixedPointAnalysisKind,
703 prepared: FixedPointAnalysisKind,
704) -> String {
705 let mut scratch = crate::error_format::ErrorFormatScratch::default();
706 let _ = std::fmt::Write::write_fmt(
707 &mut scratch.buf,
708 format_args!(
709 "{consumer} received a {resident:?} resident fixed-point graph but a {prepared:?} prepared graph. Fix: build the resident graph from the same prepared analysis plan before sequence-window dispatch."
710 ),
711 );
712 scratch.finish()
713}
714
715#[cfg(test)]
716mod resident_closure_driver_tests {
717 use super::{
718 prepare_resident_closure_frontier, resident_frontier_byte_len,
719 FixedPointResidentClosureLabels, FixedPointResidentGraph,
720 };
721 use crate::fixed_point_graph::FixedPointAnalysisKind;
722
723 const LABELS: FixedPointResidentClosureLabels = FixedPointResidentClosureLabels {
724 kind: FixedPointAnalysisKind::Reaching,
725 analysis: "test_closure",
726 capacity: "test_closure resident",
727 output: "frontier",
728 tail: "test_closure output",
729 };
730
731 fn graph(kind: FixedPointAnalysisKind, node_count: u32) -> FixedPointResidentGraph {
732 FixedPointResidentGraph {
733 kind,
734 node_count,
735 edge_count: 0,
736 layout_hash: 0,
737 resources: Vec::new(),
738 }
739 }
740
741 #[test]
742 fn resident_closure_driver_prepares_seed_once_for_all_wrappers() {
743 let graph = graph(FixedPointAnalysisKind::Reaching, 33);
744 let mut scratch = crate::fixed_point_scratch::FixedPointScratch::default();
745
746 let words = prepare_resident_closure_frontier(
747 &graph,
748 &[0x8000_0001, 1],
749 7,
750 &mut scratch,
751 LABELS,
752 "test_closure_kind",
753 )
754 .expect("Fix: shared resident driver must accept a canonical two-word frontier");
755
756 assert_eq!(words, 2);
757 assert_eq!(scratch.frontier_words, [0x8000_0001, 1]);
758 }
759
760 #[test]
761 fn resident_closure_driver_rejects_wrong_graph_family_before_dispatch() {
762 let graph = graph(FixedPointAnalysisKind::Live, 1);
763 let mut scratch = crate::fixed_point_scratch::FixedPointScratch::default();
764
765 let error = prepare_resident_closure_frontier(
766 &graph,
767 &[1],
768 1,
769 &mut scratch,
770 LABELS,
771 "test_closure_kind",
772 )
773 .expect_err("Fix: shared resident driver must reject mismatched resident graph families");
774
775 assert!(error.contains("test_closure_kind"));
776 assert!(scratch.frontier_words.is_empty());
777 }
778
779 #[test]
780 fn resident_closure_driver_rejects_dirty_tail_bits() {
781 let graph = graph(FixedPointAnalysisKind::Reaching, 33);
782 let mut scratch = crate::fixed_point_scratch::FixedPointScratch::default();
783
784 let error = prepare_resident_closure_frontier(
785 &graph,
786 &[0, 0b10],
787 1,
788 &mut scratch,
789 LABELS,
790 "test_closure_kind",
791 )
792 .expect_err("Fix: shared resident driver must reject frontier bits outside node_count");
793
794 assert!(error.contains("test_closure"));
795 assert!(scratch.frontier_words.is_empty());
796 }
797
798 #[test]
799 fn resident_closure_driver_uses_checked_frontier_byte_lengths() {
800 assert_eq!(resident_frontier_byte_len(LABELS, 0).unwrap(), 0);
801 assert_eq!(resident_frontier_byte_len(LABELS, 3).unwrap(), 12);
802
803 let error = resident_frontier_byte_len(LABELS, usize::MAX)
804 .expect_err("Fix: resident frontier byte length arithmetic must be checked");
805 assert!(error.contains("test_closure"));
806 assert!(error.contains("overflow"));
807 }
808}