Skip to main content

weir/
fixed_point_resident.rs

1//! Backend-resident fixed-point graph state.
2//!
3//! Prepared graph buffers remove host repacking. Resident graph buffers go one
4//! step further: invariant CSR inputs live behind backend resource handles so
5//! repeated fixed-point dispatches do not re-upload graph structure.
6
7#![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/// Backend-resident invariant graph inputs for fixed-point analyses.
26#[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    /// Upload invariant graph buffers once into backend-resident resources.
37    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    /// Number of graph nodes in the resident dispatch domain.
83    #[must_use]
84    pub fn node_count(&self) -> u32 {
85        self.node_count
86    }
87
88    /// Analysis family this resident graph was uploaded for.
89    #[must_use]
90    pub fn kind(&self) -> FixedPointAnalysisKind {
91        self.kind
92    }
93
94    /// Verify this resident graph layout is consumed by the analysis family
95    /// that uploaded it.
96    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    /// Number of graph edges in the resident dispatch domain.
108    #[must_use]
109    pub fn edge_count(&self) -> u32 {
110        self.edge_count
111    }
112
113    /// Stable hash of the normalized graph layout uploaded into resident buffers.
114    #[must_use]
115    pub fn stable_layout_hash(&self) -> u64 {
116        self.layout_hash
117    }
118
119    /// Verify that a prepared graph plan matches this resident graph layout.
120    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    /// Number of resident invariant graph resources.
145    #[must_use]
146    pub fn resident_resource_count(&self) -> usize {
147        self.resources.len()
148    }
149
150    /// Rebuild borrowed-frontier dispatch resources into caller-owned scratch.
151    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    /// Rebuild resident-frontier dispatch resources into caller-owned scratch.
163    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    /// Build dispatch resources with an in-place resident frontier and a
205    /// resident changed flag.
206    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    /// Free every resident resource owned by this graph.
223    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/// Static labels used by the shared resident fixed-point ping-pong loop.
323#[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/// Static labels used by the shared resident fixed-point closure driver.
331#[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
350/// Build a fixed-point resident result buffer with one shared reservation
351/// policy across all resident closure families.
352pub(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
359/// Run a resident graph closure where the mutable frontier is passed as
360/// borrowed dispatch bytes on each iteration.
361pub(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
421/// Run a resident graph closure using temporary backend-resident frontier
422/// resources for this invocation.
423pub(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
464/// Run a resident graph closure using caller-retained backend frontier
465/// resources for repeated invocations.
466pub(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
537/// Allocate a temporary resident frontier pair, run a closure, then free both
538/// resources while preserving the original run error over cleanup errors.
539pub(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
571/// Run the common resident fixed-point ping-pong frontier loop.
572pub(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                // copy_if_converged already copied into result
644            } 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}