beck_core/
repr.rs

1//! The storable shape of a [`Value`], and the binary encoding of it.
2//!
3//! # Why this is not `serde_json::Value`
4//!
5//! [`crate::core::value_to_repr`] produces a self-describing JSON tree, and that is the right thing
6//! for anything a person reads. It is the wrong thing for the log, for two reasons that compound:
7//!
8//! 1. **Size.** The JSON repr tags every scalar: `Value::Int(1)` becomes `{"$":"int","v":1}` — 19
9//!    bytes carrying eight bits. A `Toggled(id)` event is a few dozen bytes of information and
10//!    around two hundred of punctuation.
11//! 2. **Work.** Every append builds a whole `serde_json::Value` tree, then serialises it to text;
12//!    every read parses text into a tree, then walks the tree to rebuild the `Value`. Four
13//!    traversals and two allocations per event, in the one place §3.7 makes the whole system
14//!    serial.
15//!
16//! Phase 0 stored events with `postcard` and measured 7,660 events/s through Postgres
17//! ([`docs/18-phase-0-report.md`](../../../../../docs/18-phase-0-report.md) §18.3.2). Phase 1 rewrote
18//! the log against `beck_core::Value` — which the runtime must not know the shape of — and reached
19//! for JSON because it is self-describing, which is exactly what postcard is not. That was a real
20//! constraint and this module is the answer to it: a **concrete** type postcard can encode, plus
21//! total conversions to and from `Value`.
22//!
23//! # Why a second type rather than `Serialize` on `Value`
24//!
25//! `Value` carries `Arc`s, a persistent map, and three variants that are *not storable at all* —
26//! `Html`, `Attr`, `Closure`. A derived `Serialize` would have to either panic on those or invent
27//! an encoding for them, and inventing one is how a view ends up in the log
28//! ([`crate::secure`], §3.5). Making the storable subset a separate type means **the encoder cannot
29//! be handed something unstorable**: the conversion returns [`NotStorable`], at the boundary, once.
30//!
31//! # Format stability
32//!
33//! This encoding is on disk, so it is a compatibility surface. [`FORMAT`] is stamped into every
34//! store and checked on open, because a log read back under a different encoding does not fail —
35//! it produces plausible nonsense, which is the one outcome an append-only audit trail may never
36//! have.
37
38use std::sync::Arc;
39
40use serde::{Deserialize, Serialize};
41
42use crate::core::{Fields, NotStorable, Value};
43use crate::pmap::PMap;
44
45/// The on-disk format version.
46///
47/// Bump when [`Repr`]'s shape changes in a way that makes old bytes decode differently. A store
48/// stamped with a different version is refused rather than read: replay is the only description of
49/// a program's history, and a misread log is worse than an unreadable one.
50///
51/// * `1` — JSON text (Phase 1, Phase 2).
52/// * `2` — postcard over [`Repr`].
53pub const FORMAT: u32 = 2;
54
55/// A [`Value`] restricted to what may be stored, as a concrete type a non-self-describing codec can
56/// encode.
57///
58/// The variants are exactly [`crate::core::value_to_repr`]'s cases, minus the three it refuses.
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60pub enum Repr {
61    Unit,
62    Bool(bool),
63    Int(i64),
64    /// The bit pattern, as `Value` stores it — so that a round trip is exact for `NaN` and for
65    /// negative zero, which a decimal rendering is not.
66    Float(u64),
67    Str(String),
68    List(Vec<Repr>),
69    /// Pairs rather than a map, because the key is a `Repr` and most map encodings insist on
70    /// strings. Order is the `PMap`'s, which is sorted, so the encoding is canonical.
71    Map(Vec<(Repr, Repr)>),
72    Data {
73        ty: String,
74        variant: Option<String>,
75        fields: Vec<(String, Repr)>,
76    },
77}
78
79impl Repr {
80    /// The storable projection of a value, or the reason there isn't one.
81    pub fn of(v: &Value) -> Result<Repr, NotStorable> {
82        Ok(match v {
83            Value::Unit => Repr::Unit,
84            Value::Bool(b) => Repr::Bool(*b),
85            Value::Int(i) => Repr::Int(*i),
86            Value::Float(bits) => Repr::Float(*bits),
87            Value::Str(s) => Repr::Str(s.to_string()),
88            Value::List(xs) => Repr::List(xs.iter().map(Repr::of).collect::<Result<_, _>>()?),
89            Value::Map(m) => {
90                let mut pairs = Vec::with_capacity(m.len());
91                for (k, val) in m.iter() {
92                    pairs.push((Repr::of(k)?, Repr::of(val)?));
93                }
94                Repr::Map(pairs)
95            }
96            Value::Data(d) => Repr::Data {
97                ty: d.ty.to_string(),
98                variant: d.variant.as_ref().map(|v| v.to_string()),
99                fields: d
100                    .fields
101                    .iter()
102                    .map(|(k, val)| Ok((k.to_string(), Repr::of(val)?)))
103                    .collect::<Result<_, NotStorable>>()?,
104            },
105            Value::Html(_) => return Err(NotStorable { kind: "view" }),
106            Value::Attr(_) => return Err(NotStorable { kind: "attribute" }),
107            Value::Closure(_) => return Err(NotStorable { kind: "closure" }),
108        })
109    }
110
111    /// Back to a value. Total: every `Repr` denotes a `Value`, which is the point of the type.
112    pub fn to_value(&self) -> Value {
113        match self {
114            Repr::Unit => Value::Unit,
115            Repr::Bool(b) => Value::Bool(*b),
116            Repr::Int(i) => Value::Int(*i),
117            Repr::Float(bits) => Value::Float(*bits),
118            Repr::Str(s) => Value::str_(s),
119            Repr::List(xs) => Value::List(Arc::new(xs.iter().map(Repr::to_value).collect())),
120            Repr::Map(pairs) => {
121                let mut m = PMap::new();
122                for (k, v) in pairs {
123                    m = m.insert(k.to_value(), v.to_value());
124                }
125                Value::Map(m)
126            }
127            Repr::Data {
128                ty,
129                variant,
130                fields,
131            } => Value::data(
132                Arc::from(ty.as_str()),
133                variant.as_deref().map(Arc::from),
134                fields
135                    .iter()
136                    .map(|(k, v)| (Arc::from(k.as_str()), v.to_value()))
137                    .collect::<Fields>(),
138            ),
139        }
140    }
141}
142
143/// Encode a value for storage.
144pub fn to_bytes(v: &Value) -> Result<Vec<u8>, NotStorable> {
145    let repr = Repr::of(v)?;
146    // The only failure postcard has for an owned `Vec` sink is allocation, and a `Repr` built from
147    // a `Value` in memory cannot exceed it by construction.
148    Ok(postcard::to_allocvec(&repr).expect("a Repr is encodable"))
149}
150
151/// Decode a value written by [`to_bytes`].
152pub fn from_bytes(bytes: &[u8]) -> Result<Value, postcard::Error> {
153    postcard::from_bytes::<Repr>(bytes).map(|r| r.to_value())
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::core::value_to_repr;
160
161    fn sample() -> Value {
162        let mut m = PMap::new();
163        m = m.insert(Value::str_("a"), Value::Int(1));
164        m = m.insert(Value::str_("b"), Value::Bool(false));
165        Value::data(
166            Arc::from("Todo"),
167            Some(Arc::from("Added")),
168            Fields::from_iter([
169                (Arc::from("id"), Value::str_("t-1")),
170                (Arc::from("text"), Value::str_("milk")),
171                (Arc::from("n"), Value::Int(-7)),
172                (Arc::from("f"), Value::Float(f64::to_bits(1.5))),
173                (
174                    Arc::from("xs"),
175                    Value::List(Arc::new(vec![Value::Unit, Value::Int(2)])),
176                ),
177                (Arc::from("m"), Value::Map(m)),
178            ]),
179        )
180    }
181
182    #[test]
183    fn every_storable_shape_round_trips_exactly() {
184        let v = sample();
185        let bytes = to_bytes(&v).expect("it is storable");
186        assert_eq!(from_bytes(&bytes).expect("it decodes"), v);
187    }
188
189    #[test]
190    fn a_float_survives_as_its_bit_pattern() {
191        // `Value` stores the bits so that it can be `Ord`; an encoding that went through a decimal
192        // would lose `NaN`'s payload and merge `-0.0` with `0.0`, and both are map keys.
193        for bits in [
194            f64::to_bits(0.0),
195            f64::to_bits(-0.0),
196            f64::to_bits(f64::NAN),
197            f64::to_bits(f64::INFINITY),
198            f64::to_bits(0.1 + 0.2),
199        ] {
200            let v = Value::Float(bits);
201            assert_eq!(from_bytes(&to_bytes(&v).unwrap()).unwrap(), v);
202        }
203    }
204
205    #[test]
206    fn the_three_unstorable_variants_are_refused_at_the_encoder() {
207        // The same refusal `value_to_repr` makes, at the same place, for the same §3.5 reason —
208        // and now unreachable from a program that compiles, because `secure::storable` proves it.
209        let html = Value::Html(Arc::new(crate::html::Html::Text {
210            text: "x".to_string(),
211            hash: 0,
212        }));
213        assert!(to_bytes(&html).is_err());
214        assert_eq!(to_bytes(&html).unwrap_err().kind, "view");
215
216        // …and a value that merely *contains* one is refused too, because the walk is total.
217        let nested = Value::List(Arc::new(vec![Value::Int(1), html]));
218        assert!(to_bytes(&nested).is_err());
219    }
220
221    #[test]
222    fn the_binary_encoding_is_much_smaller_than_the_json_one() {
223        // The reason this module exists, as a number rather than an assertion. The margin is
224        // deliberately loose — this is a regression guard, not a benchmark; `beck bench log` is
225        // where the throughput question is answered.
226        let v = sample();
227        let binary = to_bytes(&v).expect("storable").len();
228        let json = value_to_repr(&v).expect("storable").to_string().len();
229        assert!(
230            binary * 3 < json,
231            "binary {binary} B against JSON {json} B — the encoding change stopped paying"
232        );
233    }
234
235    #[test]
236    fn the_encoding_is_canonical() {
237        // Two equal values encode to the same bytes, whatever order they were built in. The log's
238        // digest depends on it (`beck replay --verify`), and a map that iterated in insertion order
239        // would break it silently.
240        let mut a = PMap::new();
241        a = a.insert(Value::Int(2), Value::str_("b"));
242        a = a.insert(Value::Int(1), Value::str_("a"));
243        let mut b = PMap::new();
244        b = b.insert(Value::Int(1), Value::str_("a"));
245        b = b.insert(Value::Int(2), Value::str_("b"));
246        assert_eq!(
247            to_bytes(&Value::Map(a)).unwrap(),
248            to_bytes(&Value::Map(b)).unwrap()
249        );
250    }
251}