Skip to main content

lex_store/
delta.rs

1//! Delta encoding for stage canonical bytes (#261 slice 3).
2//!
3//! Each implementation lives at
4//! `<root>/stages/<sig>/implementations/<stage_id>.ast.json` —
5//! the canonical bytes of the AST, content-addressed by `stage_id`.
6//! For most edits, two consecutive stages share a long byte prefix
7//! and a long byte suffix (e.g. body changes inside the same fn
8//! shape), so storing every stage as a full file is wasteful.
9//!
10//! Slice 3 ships an opt-in delta format: when a stage's diff
11//! against an existing parent stage is below
12//! [`DELTA_RATIO_THRESHOLD`], persist `<stage_id>.delta.json`
13//! holding `(base_stage_id, common_prefix_len, common_suffix_len,
14//! middle_bytes_hex)` instead of the full bytes. Reconstruction
15//! splices `base_bytes[..prefix] + middle + base_bytes[tail..]`.
16//!
17//! Chain length is capped at [`DELTA_CHAIN_CAP`]; once a chain
18//! reaches the cap, the next stage is materialized as a full
19//! snapshot so reconstruction stays O(1) per access in the limit.
20//!
21//! # Determinism
22//!
23//! The delta format is *not* canonical — multiple `(prefix,
24//! suffix, middle)` decompositions of the same byte change are
25//! valid. We pick the largest common prefix, then the largest
26//! suffix that doesn't overlap the prefix, so the format is
27//! single-valued for a given `(base_bytes, new_bytes)` pair.
28
29use serde::{Deserialize, Serialize};
30
31/// Maximum length of a delta chain. Past this, [`encode`] yields
32/// `None` so the caller writes a full snapshot. The cap is a
33/// pragmatic balance between disk savings and reconstruction cost
34/// — 32 keeps the worst-case `get_ast` at 32 file reads + 32
35/// splices, which dominates over filesystem latency.
36pub const DELTA_CHAIN_CAP: usize = 32;
37
38/// A new stage is delta-encoded when the *middle* (non-shared)
39/// bytes are at most this fraction of the new stage's size.
40/// Below the threshold the delta is a clear win; above it, the
41/// metadata overhead and the indirection cost of reconstruction
42/// usually outweigh the byte savings.
43pub const DELTA_RATIO_THRESHOLD: f64 = 0.5;
44
45/// On-disk format of a delta-encoded stage.
46///
47/// File path: `<sig>/implementations/<stage_id>.delta.json`.
48/// The pair `(common_prefix, common_suffix)` must satisfy
49/// `prefix + suffix <= base_bytes.len()` — they describe a
50/// non-overlapping splice into the base.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct StageDelta {
53    /// `stage_id` of the previous stage these bytes are diff'd
54    /// against. Either holds full bytes (`.ast.json`) or its own
55    /// delta — reconstruction recurses.
56    pub base_stage_id: String,
57    /// Length of the chain ending at *this* stage. A delta against
58    /// a full snapshot has `chain_length: 1`; chained deltas
59    /// increment from there.
60    pub chain_length: usize,
61    /// Number of bytes shared at the start with `base`.
62    pub common_prefix: usize,
63    /// Number of bytes shared at the end with `base`.
64    pub common_suffix: usize,
65    /// Lowercase-hex of the replacement bytes for the middle.
66    /// Empty hex string means "delete the middle, splice prefix
67    /// directly to suffix" — happens when the new bytes are a
68    /// pure deletion against the base.
69    pub middle_hex: String,
70}
71
72/// Compute the splice between `base` and `new`. Always succeeds —
73/// degenerate inputs (identical bytes, total replacement) produce
74/// valid but trivial deltas. The caller decides whether the
75/// resulting middle is small enough to be worth storing as a
76/// delta versus a full snapshot.
77pub fn splice(base: &[u8], new: &[u8]) -> (usize, usize, Vec<u8>) {
78    let prefix_len = common_prefix_len(base, new);
79    // Suffix can't overlap the prefix in either side: cap at the
80    // remaining length of each.
81    let max_suffix = std::cmp::min(
82        base.len().saturating_sub(prefix_len),
83        new.len().saturating_sub(prefix_len),
84    );
85    let suffix_len = common_suffix_len(
86        &base[base.len() - max_suffix..],
87        &new[new.len() - max_suffix..],
88    );
89    let middle = new[prefix_len..new.len() - suffix_len].to_vec();
90    (prefix_len, suffix_len, middle)
91}
92
93/// Apply a splice to `base`, producing the reconstructed `new`
94/// bytes. Returns an error when the prefix+suffix lengths would
95/// overflow the base — guards against tampered or corrupt
96/// `.delta.json` files.
97pub fn apply(base: &[u8], delta: &StageDelta) -> Result<Vec<u8>, DeltaError> {
98    let middle = hex::decode(&delta.middle_hex)
99        .map_err(|e| DeltaError::InvalidHex(e.to_string()))?;
100    if delta.common_prefix + delta.common_suffix > base.len() {
101        return Err(DeltaError::OverlappingSplice {
102            base_len: base.len(),
103            prefix: delta.common_prefix,
104            suffix: delta.common_suffix,
105        });
106    }
107    let prefix = &base[..delta.common_prefix];
108    let suffix_start = base.len() - delta.common_suffix;
109    let suffix = &base[suffix_start..];
110    let mut out = Vec::with_capacity(prefix.len() + middle.len() + suffix.len());
111    out.extend_from_slice(prefix);
112    out.extend(middle);
113    out.extend_from_slice(suffix);
114    Ok(out)
115}
116
117/// Decide whether the delta is worth storing. `middle_len` is the
118/// length of the splice's replacement bytes; `new_len` is the
119/// total length of the new bytes. Returns `true` when the ratio
120/// is below [`DELTA_RATIO_THRESHOLD`] *and* the chain length isn't
121/// at the cap.
122pub fn is_worth_encoding(middle_len: usize, new_len: usize, chain_length: usize) -> bool {
123    if chain_length > DELTA_CHAIN_CAP {
124        return false;
125    }
126    if new_len == 0 {
127        return false;
128    }
129    (middle_len as f64) / (new_len as f64) < DELTA_RATIO_THRESHOLD
130}
131
132fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
133    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
134}
135
136fn common_suffix_len(a: &[u8], b: &[u8]) -> usize {
137    a.iter()
138        .rev()
139        .zip(b.iter().rev())
140        .take_while(|(x, y)| x == y)
141        .count()
142}
143
144#[derive(Debug, thiserror::Error)]
145pub enum DeltaError {
146    #[error("delta middle is not valid hex: {0}")]
147    InvalidHex(String),
148    #[error("delta splice overflows base: base_len={base_len}, prefix={prefix}, suffix={suffix}")]
149    OverlappingSplice {
150        base_len: usize,
151        prefix: usize,
152        suffix: usize,
153    },
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn splice_then_apply_round_trips() {
162        let base = b"hello, the quick brown fox jumps over the lazy dog";
163        let new = b"hello, the quick green fox jumps over the lazy dog";
164        let (p, s, m) = splice(base, new);
165        let delta = StageDelta {
166            base_stage_id: "x".into(),
167            chain_length: 1,
168            common_prefix: p,
169            common_suffix: s,
170            middle_hex: hex::encode(&m),
171        };
172        let reconstructed = apply(base, &delta).unwrap();
173        assert_eq!(reconstructed, new);
174    }
175
176    #[test]
177    fn identical_bytes_yield_empty_middle() {
178        let base = b"unchanged";
179        let new = b"unchanged";
180        let (p, s, m) = splice(base, new);
181        assert_eq!(p, 9);
182        assert_eq!(s, 0);
183        assert!(m.is_empty(), "no middle when bytes are identical");
184        // Apply gives back base.
185        let delta = StageDelta {
186            base_stage_id: "x".into(),
187            chain_length: 1,
188            common_prefix: p,
189            common_suffix: s,
190            middle_hex: hex::encode(&m),
191        };
192        assert_eq!(apply(base, &delta).unwrap(), base);
193    }
194
195    #[test]
196    fn pure_insertion_is_pure_middle() {
197        let base = b"abXY";
198        let new = b"abZZZZXY";
199        let (p, s, m) = splice(base, new);
200        assert_eq!(p, 2);
201        assert_eq!(s, 2);
202        assert_eq!(m, b"ZZZZ");
203    }
204
205    #[test]
206    fn pure_deletion_yields_empty_middle() {
207        let base = b"abZZZZXY";
208        let new = b"abXY";
209        let (p, s, m) = splice(base, new);
210        assert_eq!(p, 2);
211        assert_eq!(s, 2);
212        assert!(m.is_empty());
213        let delta = StageDelta {
214            base_stage_id: "x".into(),
215            chain_length: 1,
216            common_prefix: p,
217            common_suffix: s,
218            middle_hex: hex::encode(&m),
219        };
220        assert_eq!(apply(base, &delta).unwrap(), new);
221    }
222
223    #[test]
224    fn is_worth_encoding_respects_threshold() {
225        // Middle is 30% of new — under 50% threshold.
226        assert!(is_worth_encoding(30, 100, 1));
227        // 60% — over threshold.
228        assert!(!is_worth_encoding(60, 100, 1));
229        // Chain length cap.
230        assert!(!is_worth_encoding(1, 100, DELTA_CHAIN_CAP + 1));
231    }
232
233    #[test]
234    fn apply_refuses_overlapping_splice() {
235        let base = b"short";
236        let delta = StageDelta {
237            base_stage_id: "x".into(),
238            chain_length: 1,
239            common_prefix: 4,
240            common_suffix: 4,  // 4 + 4 > 5 — invalid
241            middle_hex: String::new(),
242        };
243        assert!(apply(base, &delta).is_err());
244    }
245}