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) => {
89                let mut out = Vec::with_capacity(xs.len());
90                xs.try_for_each(|x| Repr::of(x).map(|r| out.push(r)))?;
91                Repr::List(out)
92            }
93            Value::Map(m) => {
94                let mut pairs = Vec::with_capacity(m.len());
95                for (k, val) in m.iter() {
96                    pairs.push((Repr::of(k)?, Repr::of(val)?));
97                }
98                Repr::Map(pairs)
99            }
100            Value::Data(d) => Repr::Data {
101                ty: d.ty.to_string(),
102                variant: d.variant.as_ref().map(|v| v.to_string()),
103                fields: d
104                    .fields
105                    .iter()
106                    .map(|(k, val)| Ok((k.to_string(), Repr::of(val)?)))
107                    .collect::<Result<_, NotStorable>>()?,
108            },
109            Value::Html(_) => return Err(NotStorable { kind: "view" }),
110            Value::Attr(_) => return Err(NotStorable { kind: "attribute" }),
111            Value::Closure(_) => return Err(NotStorable { kind: "closure" }),
112        })
113    }
114
115    /// Back to a value. Total: every `Repr` denotes a `Value`, which is the point of the type.
116    pub fn to_value(&self) -> Value {
117        match self {
118            Repr::Unit => Value::Unit,
119            Repr::Bool(b) => Value::Bool(*b),
120            Repr::Int(i) => Value::Int(*i),
121            Repr::Float(bits) => Value::Float(*bits),
122            Repr::Str(s) => Value::str_(s),
123            Repr::List(xs) => Value::list(xs.iter().map(Repr::to_value).collect()),
124            Repr::Map(pairs) => {
125                let mut m = PMap::new();
126                for (k, v) in pairs {
127                    m = m.insert(k.to_value(), v.to_value());
128                }
129                Value::Map(m)
130            }
131            Repr::Data {
132                ty,
133                variant,
134                fields,
135            } => Value::data(
136                Arc::from(ty.as_str()),
137                variant.as_deref().map(Arc::from),
138                fields
139                    .iter()
140                    .map(|(k, v)| (Arc::from(k.as_str()), v.to_value()))
141                    .collect::<Fields>(),
142            ),
143        }
144    }
145}
146
147/// Encode a value for storage.
148pub fn to_bytes(v: &Value) -> Result<Vec<u8>, NotStorable> {
149    let repr = Repr::of(v)?;
150    // The only failure postcard has for an owned `Vec` sink is allocation, and a `Repr` built from
151    // a `Value` in memory cannot exceed it by construction.
152    Ok(postcard::to_allocvec(&repr).expect("a Repr is encodable"))
153}
154
155/// Decode a value written by [`to_bytes`].
156pub fn from_bytes(bytes: &[u8]) -> Result<Value, postcard::Error> {
157    postcard::from_bytes::<Repr>(bytes).map(|r| r.to_value())
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::core::value_to_repr;
164
165    fn sample() -> Value {
166        let mut m = PMap::new();
167        m = m.insert(Value::str_("a"), Value::Int(1));
168        m = m.insert(Value::str_("b"), Value::Bool(false));
169        Value::data(
170            Arc::from("Todo"),
171            Some(Arc::from("Added")),
172            Fields::from_iter([
173                (Arc::from("id"), Value::str_("t-1")),
174                (Arc::from("text"), Value::str_("milk")),
175                (Arc::from("n"), Value::Int(-7)),
176                (Arc::from("f"), Value::Float(f64::to_bits(1.5))),
177                (
178                    Arc::from("xs"),
179                    Value::list(vec![Value::Unit, Value::Int(2)]),
180                ),
181                (Arc::from("m"), Value::Map(m)),
182            ]),
183        )
184    }
185
186    #[test]
187    fn every_storable_shape_round_trips_exactly() {
188        let v = sample();
189        let bytes = to_bytes(&v).expect("it is storable");
190        assert_eq!(from_bytes(&bytes).expect("it decodes"), v);
191    }
192
193    #[test]
194    fn a_float_survives_as_its_bit_pattern() {
195        // `Value` stores the bits so that it can be `Ord`; an encoding that went through a decimal
196        // would lose `NaN`'s payload and merge `-0.0` with `0.0`, and both are map keys.
197        for bits in [
198            f64::to_bits(0.0),
199            f64::to_bits(-0.0),
200            f64::to_bits(f64::NAN),
201            f64::to_bits(f64::INFINITY),
202            f64::to_bits(0.1 + 0.2),
203        ] {
204            let v = Value::Float(bits);
205            assert_eq!(from_bytes(&to_bytes(&v).unwrap()).unwrap(), v);
206        }
207    }
208
209    #[test]
210    fn the_three_unstorable_variants_are_refused_at_the_encoder() {
211        // The same refusal `value_to_repr` makes, at the same place, for the same §3.5 reason —
212        // and now unreachable from a program that compiles, because `secure::storable` proves it.
213        let html = Value::Html(Arc::new(crate::html::Html::Text {
214            text: "x".to_string(),
215            hash: 0,
216        }));
217        assert!(to_bytes(&html).is_err());
218        assert_eq!(to_bytes(&html).unwrap_err().kind, "view");
219
220        // …and a value that merely *contains* one is refused too, because the walk is total.
221        let nested = Value::list(vec![Value::Int(1), html]);
222        assert!(to_bytes(&nested).is_err());
223    }
224
225    #[test]
226    fn the_binary_encoding_is_much_smaller_than_the_json_one() {
227        // The reason this module exists, as a number rather than an assertion. The margin is
228        // deliberately loose — this is a regression guard, not a benchmark; `beck bench log` is
229        // where the throughput question is answered.
230        let v = sample();
231        let binary = to_bytes(&v).expect("storable").len();
232        let json = value_to_repr(&v).expect("storable").to_string().len();
233        assert!(
234            binary * 3 < json,
235            "binary {binary} B against JSON {json} B — the encoding change stopped paying"
236        );
237    }
238
239    #[test]
240    fn the_encoding_is_canonical() {
241        // Two equal values encode to the same bytes, whatever order they were built in. The log's
242        // digest depends on it (`beck replay --verify`), and a map that iterated in insertion order
243        // would break it silently.
244        let mut a = PMap::new();
245        a = a.insert(Value::Int(2), Value::str_("b"));
246        a = a.insert(Value::Int(1), Value::str_("a"));
247        let mut b = PMap::new();
248        b = b.insert(Value::Int(1), Value::str_("a"));
249        b = b.insert(Value::Int(2), Value::str_("b"));
250        assert_eq!(
251            to_bytes(&Value::Map(a)).unwrap(),
252            to_bytes(&Value::Map(b)).unwrap()
253        );
254    }
255}