Skip to main content

delta/
inplace.rs

1use std::cmp::Reverse;
2use std::collections::BinaryHeap;
3
4use crate::types::{Command, CyclePolicy, PlacedCommand};
5
6// ── DFS color states for find_cycle_in_scc ──────────────────────────────────
7const COLOR_UNVISITED: u8 = 0;
8const COLOR_ON_PATH:   u8 = 1;
9const COLOR_DONE:      u8 = 2;
10
11/// Sentinel value meaning "vertex is in no non-trivial SCC".
12const NO_SCC: usize = usize::MAX;
13
14// ── Data structures ──────────────────────────────────────────────────────────
15
16/// Source offset, destination offset, and length of one copy command.
17#[derive(Clone, Copy)]
18struct CopyInfo {
19    src:    usize,
20    dst:    usize,
21    length: usize,
22}
23
24/// Non-trivial SCCs with per-SCC active counts and vertex-to-SCC mapping.
25struct SccList {
26    sccs:   Vec<Vec<usize>>,  // non-trivial SCCs only
27    active: Vec<usize>,        // live member count per SCC
28    id:     Vec<usize>,        // vertex → SCC index; NO_SCC = trivial
29}
30
31/// Statistics from in-place conversion.
32#[derive(Debug, Default)]
33pub struct InplaceStats {
34    pub num_copies:       usize,
35    pub num_adds:         usize,
36    pub edges:            usize,
37    pub cycles_broken:    usize,
38    pub copies_converted: usize,
39    pub bytes_converted:  usize,
40}
41
42// ── Tarjan SCC (unchanged) ───────────────────────────────────────────────────
43
44/// Compute SCCs using iterative Tarjan's algorithm.
45///
46/// Returns SCCs in reverse topological order (sinks first); caller reverses
47/// for source-first processing order.
48///
49/// R.E. Tarjan, "Depth-first search and linear graph algorithms,"
50/// SIAM J. Comput., 1(2):146-160, June 1972.
51fn tarjan_scc(adj: &[Vec<usize>], n: usize) -> Vec<Vec<usize>> {
52    let mut index_counter = 0usize;
53    let mut index = vec![usize::MAX; n]; // MAX = unvisited
54    let mut lowlink = vec![0usize; n];
55    let mut on_stack = vec![false; n];
56    let mut tarjan_stack: Vec<usize> = Vec::new();
57    let mut sccs: Vec<Vec<usize>> = Vec::new();
58    // DFS call stack: (vertex, next_neighbor_index)
59    let mut call_stack: Vec<(usize, usize)> = Vec::new();
60
61    for start in 0..n {
62        if index[start] != usize::MAX {
63            continue;
64        }
65
66        index[start] = index_counter;
67        lowlink[start] = index_counter;
68        index_counter += 1;
69        on_stack[start] = true;
70        tarjan_stack.push(start);
71        call_stack.push((start, 0));
72
73        while let Some(&(v, ni)) = call_stack.last() {
74            if ni < adj[v].len() {
75                let w = adj[v][ni];
76                call_stack.last_mut().unwrap().1 += 1;
77                if index[w] == usize::MAX {
78                    // Tree edge: descend into w
79                    index[w] = index_counter;
80                    lowlink[w] = index_counter;
81                    index_counter += 1;
82                    on_stack[w] = true;
83                    tarjan_stack.push(w);
84                    call_stack.push((w, 0));
85                } else if on_stack[w] {
86                    // Back-edge into current SCC
87                    if index[w] < lowlink[v] {
88                        lowlink[v] = index[w];
89                    }
90                }
91            } else {
92                call_stack.pop();
93                if let Some(&(parent, _)) = call_stack.last() {
94                    if lowlink[v] < lowlink[parent] {
95                        lowlink[parent] = lowlink[v];
96                    }
97                }
98                if lowlink[v] == index[v] {
99                    let mut scc = Vec::new();
100                    loop {
101                        let w = tarjan_stack.pop().unwrap();
102                        on_stack[w] = false;
103                        scc.push(w);
104                        if w == v {
105                            break;
106                        }
107                    }
108                    sccs.push(scc);
109                }
110            }
111        }
112    }
113
114    sccs // sinks first; caller reverses for source-first order
115}
116
117// ── Cycle finder (unchanged interface, improved constants) ───────────────────
118
119/// Find a cycle in the active subgraph of one SCC.
120///
121/// Designed for repeated calls within the same SCC between cycle breakings:
122///
123/// - `sid` / `scc_id`: replaces a scc_member[] bool array; O(1) per neighbor
124///   check instead of O(|SCC|) set-then-clear per call.
125/// - `color[]` is persistent across calls (COLOR_DONE entries from fully
126///   explored vertices are not reset): total DFS work across all calls within
127///   one SCC is O(|SCC| + E_SCC), not O(|SCC| × cycles_broken).
128/// - `scan_start`: amortized outer-loop position; advances monotonically so
129///   the total scan cost per SCC is O(|SCC|), not O(|SCC| × cycles_broken).
130///
131/// On cycle found: resets COLOR_ON_PATH path vertices to COLOR_UNVISITED so
132/// they can be re-examined after victim removal; leaves COLOR_DONE intact.
133/// On None: COLOR_DONE values persist; caller advances scc_ptr without cleanup.
134fn find_cycle_in_scc(
135    adj: &[Vec<usize>],
136    scc: &[usize],
137    sid: usize,
138    scc_id: &[usize],
139    removed: &[bool],
140    color: &mut [u8],
141    scan_start: &mut usize,
142) -> Option<Vec<usize>> {
143    let mut path: Vec<usize> = Vec::new();
144
145    while *scan_start < scc.len() {
146        let start = scc[*scan_start];
147        if removed[start] || color[start] != COLOR_UNVISITED {
148            *scan_start += 1;
149            continue;
150        }
151
152        color[start] = COLOR_ON_PATH;
153        path.push(start);
154        let mut stack: Vec<(usize, usize)> = vec![(start, 0)];
155
156        while !stack.is_empty() {
157            let (v, ni) = *stack.last().unwrap();
158            let mut next_ni = ni;
159            let mut advanced = false;
160
161            while next_ni < adj[v].len() {
162                let w = adj[v][next_ni];
163                next_ni += 1;
164                if scc_id[w] != sid || removed[w] {
165                    continue;
166                }
167                if color[w] == COLOR_ON_PATH {
168                    // Back-edge: cycle found
169                    let pos = path.iter().position(|&x| x == w).unwrap();
170                    let cycle = path[pos..].to_vec();
171                    for &u in &path {
172                        color[u] = COLOR_UNVISITED;
173                    }
174                    return Some(cycle);
175                }
176                if color[w] == COLOR_UNVISITED {
177                    stack.last_mut().unwrap().1 = next_ni;
178                    color[w] = COLOR_ON_PATH;
179                    path.push(w);
180                    stack.push((w, 0));
181                    advanced = true;
182                    break;
183                }
184            }
185
186            if !advanced {
187                stack.pop();
188                color[v] = COLOR_DONE; // Fully explored — persists across calls.
189                path.pop();
190            }
191        }
192
193        // start's entire reachable SCC-subgraph explored; no cycle.
194        *scan_start += 1;
195    }
196
197    None
198}
199
200// ── Extracted helpers ────────────────────────────────────────────────────────
201
202/// Build CRWI digraph on copy commands.
203///
204/// Edge i→j means copy i reads from a region that copy j will overwrite,
205/// so i must execute before j.  O(n log n + E) sweep-line construction.
206/// Returns (adj, in_deg).
207fn build_crwi_digraph(
208    copy_info: &[CopyInfo],
209    n: usize,
210    stats: &mut InplaceStats,
211) -> (Vec<Vec<usize>>, Vec<usize>) {
212    let mut adj:    Vec<Vec<usize>> = vec![Vec::new(); n];
213    let mut in_deg: Vec<usize>      = vec![0; n];
214
215    // Sort copy write-intervals by start; binary-search for each read interval.
216    let mut write_sorted: Vec<usize> = (0..n).collect();
217    write_sorted.sort_unstable_by_key(|&j| copy_info[j].dst);
218    let write_starts: Vec<usize> = write_sorted.iter().map(|&j| copy_info[j].dst).collect();
219
220    for i in 0..n {
221        let src      = copy_info[i].src;
222        let length   = copy_info[i].length;
223        let read_end = src + length;
224        // lo = first write with dst >= src; hi = first write with dst >= read_end.
225        // Writes in [lo, hi) start inside [src, read_end) — they always overlap.
226        // The write at lo-1 starts before src; overlaps iff its end exceeds src.
227        let lo = write_starts.partition_point(|&ws| ws < src);
228        let hi = write_starts.partition_point(|&ws| ws < read_end);
229        if lo > 0 {
230            let j = write_sorted[lo - 1];
231            if j != i {
232                let dj = copy_info[j].dst;
233                let lj = copy_info[j].length;
234                if dj + lj > src {
235                    adj[i].push(j);
236                    in_deg[j] += 1;
237                    stats.edges += 1;
238                }
239            }
240        }
241        for k in lo..hi {
242            let j = write_sorted[k];
243            if j != i {
244                adj[i].push(j);
245                in_deg[j] += 1;
246                stats.edges += 1;
247            }
248        }
249    }
250    (adj, in_deg)
251}
252
253/// Wrap tarjan_scc output into an SccList containing only non-trivial SCCs.
254fn build_scc_list(adj: &[Vec<usize>], n: usize) -> SccList {
255    let sccs_raw = tarjan_scc(adj, n);
256    let mut id     = vec![NO_SCC; n];
257    let mut sccs:   Vec<Vec<usize>> = Vec::new();
258    let mut active: Vec<usize>      = Vec::new();
259
260    for scc in &sccs_raw {
261        if scc.len() > 1 {
262            let sid = sccs.len();
263            for &v in scc {
264                id[v] = sid;
265            }
266            active.push(scc.len());
267            sccs.push(scc.clone());
268        }
269    }
270    SccList { sccs, active, id }
271}
272
273/// Select a victim copy to break a cycle when Kahn's algorithm stalls.
274///
275/// Constant: first remaining vertex.  Localmin: minimum-length copy in a cycle.
276/// scc_ptr and scan_pos are advanced in place across repeated calls.
277fn pick_victim(
278    copy_info: &[CopyInfo],
279    adj:       &[Vec<usize>],
280    scc_list:  &mut SccList,
281    removed:   &[bool],
282    color:     &mut [u8],
283    scc_ptr:   &mut usize,
284    scan_pos:  &mut usize,
285    policy:    CyclePolicy,
286    n:         usize,
287) -> usize {
288    match policy {
289        CyclePolicy::Constant => (0..n).find(|&i| !removed[i]).unwrap(),
290        CyclePolicy::Localmin => loop {
291            while *scc_ptr < scc_list.sccs.len() && scc_list.active[*scc_ptr] == 0 {
292                *scc_ptr += 1;
293                *scan_pos = 0;
294            }
295            if *scc_ptr >= scc_list.sccs.len() {
296                // Safety fallback — should not happen with a correct graph.
297                break (0..n).find(|&i| !removed[i]).unwrap();
298            }
299            let result = find_cycle_in_scc(
300                adj,
301                &scc_list.sccs[*scc_ptr],
302                *scc_ptr,
303                &scc_list.id,
304                removed,
305                color,
306                scan_pos,
307            );
308            match result {
309                Some(cycle) => {
310                    break *cycle.iter().min_by_key(|&&i| (copy_info[i].length, i)).unwrap();
311                }
312                None => {
313                    // This SCC's remaining subgraph is acyclic; advance.
314                    *scc_ptr += 1;
315                    *scan_pos = 0;
316                }
317            }
318        },
319    }
320}
321
322/// Run Kahn topological sort; when the heap stalls, call pick_victim to break
323/// the cycle by materialising one copy as a literal add.
324fn run_kahn(
325    copy_info: &[CopyInfo],
326    adj:       &[Vec<usize>],
327    scc_list:  &mut SccList,
328    in_deg:    &mut [usize],
329    r:         &[u8],
330    add_info:  &mut Vec<(usize, Vec<u8>)>,
331    policy:    CyclePolicy,
332    n:         usize,
333    stats:     &mut InplaceStats,
334) -> Vec<usize> {
335    let mut removed    = vec![false; n];
336    let mut topo_order = Vec::with_capacity(n);
337    let mut color      = vec![COLOR_UNVISITED; n];
338    let mut scc_ptr    = 0usize;
339    let mut scan_pos   = 0usize;
340
341    let mut heap: BinaryHeap<Reverse<(usize, usize)>> = BinaryHeap::new();
342    for i in 0..n {
343        if in_deg[i] == 0 {
344            heap.push(Reverse((copy_info[i].length, i)));
345        }
346    }
347    let mut processed = 0;
348
349    while processed < n {
350        // Drain all ready vertices.
351        while let Some(Reverse((_, v))) = heap.pop() {
352            if removed[v] {
353                continue;
354            }
355            removed[v] = true;
356            topo_order.push(v);
357            processed += 1;
358            if scc_list.id[v] != NO_SCC {
359                scc_list.active[scc_list.id[v]] -= 1;
360            }
361            for &w in &adj[v] {
362                if !removed[w] {
363                    in_deg[w] -= 1;
364                    if in_deg[w] == 0 {
365                        heap.push(Reverse((copy_info[w].length, w)));
366                    }
367                }
368            }
369        }
370
371        if processed >= n {
372            break;
373        }
374
375        // Kahn stalled: all remaining vertices are in CRWI cycles.
376        let victim = pick_victim(
377            copy_info, adj, scc_list, &removed, &mut color,
378            &mut scc_ptr, &mut scan_pos, policy, n,
379        );
380        let ci = copy_info[victim];
381        add_info.push((ci.dst, r[ci.src..ci.src + ci.length].to_vec()));
382        stats.cycles_broken    += 1;
383        stats.copies_converted += 1;
384        stats.bytes_converted  += ci.length;
385        removed[victim] = true;
386        processed += 1;
387        if scc_list.id[victim] != NO_SCC {
388            scc_list.active[scc_list.id[victim]] -= 1;
389        }
390        for &w in &adj[victim] {
391            if !removed[w] {
392                in_deg[w] -= 1;
393                if in_deg[w] == 0 {
394                    heap.push(Reverse((copy_info[w].length, w)));
395                }
396            }
397        }
398    }
399    topo_order
400}
401
402// ── Public entry point ───────────────────────────────────────────────────────
403
404/// Convert standard delta commands to in-place executable commands.
405///
406/// The returned commands can be applied to a buffer initialized with R
407/// to reconstruct V in-place, without a separate output buffer.
408///
409/// Why overlaps don't always require add conversion:
410/// When copy i reads from `[src_i, src_i+len_i)` and copy j writes to
411/// `[dst_j, dst_j+len_j)`, and these intervals overlap, copy i MUST execute
412/// before j overwrites its source data.  This ordering constraint is an edge
413/// i→j in the CRWI (Copy-Read/Write-Intersection) digraph.  When the graph
414/// is acyclic, a topological order gives a valid serial schedule — no
415/// conversion needed.  A cycle i₁→i₂→…→iₖ→i₁ creates a circular dependency
416/// with no valid schedule; breaking it materializes one copy as a literal add
417/// (saving source bytes from R before the buffer is modified).
418///
419/// Algorithm (Burns, Long, Stockmeyer, IEEE TKDE 2003):
420///   1. Annotate each command with its write offset in the output
421///   2. Build CRWI digraph: edge i→j iff i's read interval intersects j's
422///      write interval (Section 4.2)
423///   3. Topological sort (Kahn); when the heap empties with nodes remaining,
424///      a cycle exists — find it and convert the minimum-length copy to an add
425///   4. Output: copies in topological order, then all adds
426pub fn make_inplace(
427    r: &[u8],
428    commands: &[Command],
429    policy: CyclePolicy,
430) -> (Vec<PlacedCommand>, InplaceStats) {
431    let mut stats = InplaceStats::default();
432
433    if commands.is_empty() {
434        return (Vec::new(), stats);
435    }
436
437    // Step 1: compute write offsets
438    let mut copy_info: Vec<CopyInfo>       = Vec::new();
439    let mut add_info:  Vec<(usize, Vec<u8>)> = Vec::new();
440    let mut write_pos = 0usize;
441
442    for cmd in commands {
443        match cmd {
444            Command::Copy { offset, length } => {
445                copy_info.push(CopyInfo { src: *offset, dst: write_pos, length: *length });
446                write_pos += length;
447            }
448            Command::Add { data } => {
449                add_info.push((write_pos, data.clone()));
450                write_pos += data.len();
451            }
452        }
453    }
454
455    let n = copy_info.len();
456    if n == 0 {
457        stats.num_adds = add_info.len();
458        return (
459            add_info
460                .into_iter()
461                .map(|(dst, data)| PlacedCommand::Add { dst, data })
462                .collect(),
463            stats,
464        );
465    }
466
467    // Steps 2-3: build digraph, topological sort, break cycles
468    let (adj, mut in_deg) = build_crwi_digraph(&copy_info, n, &mut stats);
469    let mut scc_list      = build_scc_list(&adj, n);
470    let topo_order        = run_kahn(
471        &copy_info, &adj, &mut scc_list, &mut in_deg,
472        r, &mut add_info, policy, n, &mut stats,
473    );
474
475    // Step 4: assemble result — copies in topo order, then all adds
476    stats.num_copies = topo_order.len();
477    let mut result: Vec<PlacedCommand> = Vec::new();
478    for &i in &topo_order {
479        let ci = copy_info[i];
480        result.push(PlacedCommand::Copy { src: ci.src, dst: ci.dst, length: ci.length });
481    }
482    for (dst, data) in add_info {
483        result.push(PlacedCommand::Add { dst, data });
484    }
485    stats.num_adds = result.len() - stats.num_copies;
486
487    (result, stats)
488}