logicaffeine_compile/vm/journal.rs
1//! The history tape — the Bennett/Landauer "garbage" of reverse execution.
2//!
3//! A forward run of the VM produces `(final state, tape)`; [`Vm::run_reverse`](crate::vm::Vm) walks
4//! the instruction stream backward and consumes the tape to reconstruct the EXACT initial state.
5//! Each information-losing op appends the minimal destroyed bits (an undo-log delta, NOT a full
6//! snapshot); a [`RevClass::Bijection`](crate::vm::reverse::RevClass) op appends NOTHING — it
7//! self-inverts algebraically. So the tape's size is the erased-bit (Landauer energy) cost, and it
8//! is ZERO for an all-abelian run — the zero-erasure ideal the adiabatic chip is built on.
9//! Bennett uncompute (R4) shrinks it further toward that floor.
10
11use crate::vm::value::Value;
12
13/// One recorded delta on the [`HistoryTape`]. Records only what a forward op DESTROYED, so reverse
14/// execution can restore it. Zero-cost/control entries (a shown line, a branch direction) carry no
15/// erased data — they are re-derivable — and so contribute nothing to the erased-bit measure.
16#[derive(Debug)]
17pub enum JournalEntry {
18 /// A register slot (`base + r`) was overwritten; restore `old` on reverse.
19 RegOverwrite { slot: usize, old: Value },
20 /// A promoted global was overwritten (or first-defined, `old == None`); restore it on reverse.
21 GlobalOverwrite { idx: usize, old: Option<Value> },
22 /// Several register slots were overwritten at once (a tuple destructure). Reverse restores each.
23 MultiRegOverwrite { slots: Vec<(usize, Value)> },
24 /// A `Show` appended one output line; reverse truncates the output buffer. The content is
25 /// re-derivable on a re-forward, so no data is erased.
26 ShowLine,
27 /// A NONDETERMINISTIC read (clock / random) drew `drawn` into a register. Reading does not mutate
28 /// the world, so reverse simply restores the register's prior value (`dst_old`). Critically,
29 /// `drawn` is RECORDED — the entropy is captured on the tape (a deterministic replay could feed it
30 /// back), rather than silently discarded, which is what makes reversing a nondeterministic program
31 /// honest rather than a lie.
32 NondetRead {
33 dst_slot: usize,
34 dst_old: Value,
35 drawn: Value,
36 },
37 /// A conditional branch was taken (`true`) or fell through (`false`); reverse follows it back.
38 /// One bit of control flow, not erased data.
39 Branch { taken: bool },
40 /// An in-place collection mutation (push / index-set / add / remove). `old` is the register's
41 /// pre-mutation value — an `Rc` bump for a shared List/Map/Set, or a deep copy for a by-value
42 /// container. Because that bump forces the op's copy-on-write, the original stays pristine, so
43 /// reverse restores `old` exactly (bit-identical, including aliasing). If instead the register
44 /// still holds the SAME `Rc` (an in-place mutation — only when value-semantics is off), reverse
45 /// declines rather than mis-restore.
46 CollectionMutate { slot: usize, old: Value },
47 /// A `ListPop`: it both shrinks a list AND overwrites a destination register. Reverse restores
48 /// the pristine pre-pop list (`list_old`, which — COW-forced — still holds the popped element)
49 /// and the destination's prior value (`dst_old`), so no push-back or empty-flag is needed.
50 ListPop {
51 list_slot: usize,
52 list_old: Value,
53 dst_slot: usize,
54 dst_old: Value,
55 },
56 /// An `IterNext`. `advanced` = it yielded an element (idx < len): reverse restores the
57 /// destination's prior value and rewinds the top iterator's index by one. `!advanced` = it was
58 /// exhausted (a pure jump to the loop exit): reverse does nothing.
59 IterNextUndo {
60 advanced: bool,
61 dst_slot: usize,
62 dst_old: Value,
63 },
64 /// An `IterPop` dropped the top iterator (a snapshot + cursor). Reverse re-pushes it so the loop
65 /// body's `IterNext`s can be rewound.
66 IterPopUndo { items: Vec<Value>, idx: usize },
67 /// A CRDT interior mutation (`CrdtAppend`/`CrdtResolve` on an `Rc<RefCell<CrdtValue>>`). Unlike a
68 /// by-value container, the mutation happens IN PLACE through the shared handle, so an `Rc`-bump
69 /// snapshot would share the mutated interior, not preserve it. Instead `snapshot` is a DEEP clone
70 /// of the pre-mutation `CrdtValue` (wrapped in a fresh handle); reverse copies it back THROUGH the
71 /// register's live handle (`*rc.borrow_mut() = …`), so every alias observes the restore.
72 CrdtInterior { slot: usize, snapshot: Value },
73 /// A `CrdtResolve` onto a nested CRDT-register FIELD (`registers[obj_slot]`'s `field`) — the
74 /// field's `Rc<RefCell<CrdtValue>>` interior is resolved in place. Like
75 /// [`JournalEntry::CrdtInterior`] but the handle lives inside a struct field, so `snapshot` (a
76 /// deep pre-resolve clone) is copied back through that field's live handle. (A resolve onto a
77 /// plain field is a `CollectionMutate` instead.)
78 CrdtFieldInterior {
79 obj_slot: usize,
80 field: String,
81 snapshot: Value,
82 },
83 /// An IN-PLACE integer division `a = a / b` — the quotient is kept in `dst_slot`, the REMAINDER is
84 /// the only bit truly discarded. Reverse reconstructs `a_old = q·b + r` from the kept quotient and
85 /// the divisor, so the Landauer cost is the remainder's few bits (`< log₂ b`), not the whole
86 /// dividend word. The divisor is a recomputable input (stored here for a self-contained reverse,
87 /// but it is not erased information, so it costs nothing).
88 DivReconstruct {
89 dst_slot: usize,
90 divisor: Value,
91 remainder: Value,
92 },
93 /// An IN-PLACE bit shift (`a = a << k` / `a = a >> k`; `Shl`/`Shr`/`DivPow2` with `dst == lhs`).
94 /// Only the shifted-OUT bits are destroyed — the rest of `a_old` survives in the kept result — so
95 /// the tape records just those `k` bits (`dropped`: the low `k` bits for a right shift, the top `k`
96 /// bits right-aligned for a left shift). Reverse recombines them with the kept result, so the
97 /// Landauer cost is `k` bits, not the whole 64-bit word.
98 ShiftReconstruct {
99 dst_slot: usize,
100 dropped: i64,
101 k: u32,
102 left: bool,
103 },
104 /// An IN-PLACE bitwise mask (`a = a & b` / `a = a | b`; `BitAnd`/`BitOr` with `dst == lhs`). The
105 /// bits the live mask `b` cannot recover are `saved`: for AND the bits where `b = 0`, for OR the
106 /// bits where `b = 1`. Reverse re-merges them under the complementary mask. The Landauer cost is
107 /// exactly the count of unrecoverable positions (`64 − popcount b` for AND, `popcount b` for OR).
108 MaskReconstruct {
109 dst_slot: usize,
110 saved: i64,
111 mask: i64,
112 and: bool,
113 },
114 /// An IN-PLACE integer modulo (`a = a % b`; `Mod` with `dst == lhs`). The remainder is kept in the
115 /// destination; the QUOTIENT is the discarded information. Reverse rebuilds `a_old = q·b + r` from
116 /// the saved quotient and the live divisor. The Landauer cost is the quotient's magnitude bits.
117 ModReconstruct {
118 dst_slot: usize,
119 quotient: i64,
120 divisor: Value,
121 },
122 /// A `Call` grew the register file (a callee window). Reverse truncates it back to `restore_len`
123 /// after the callee's body has been reversed.
124 CallUndo { restore_len: usize },
125 /// A `CallValue` (closure invocation): like `Call` it grew the register file, and it also bound
126 /// captures into the callee frame. `captures` are the pre-binding `(slot, old_value)` pairs (an
127 /// overlap capture slot survives the truncate, so its prior caller value must be restored).
128 /// Reverse restores the capture slots, then truncates back to `restore_len`.
129 CallValueUndo {
130 restore_len: usize,
131 captures: Vec<(usize, Value)>,
132 },
133 /// A `Return`/`ReturnNothing` tore down the callee frame. Reverse rebuilds it: regrow the
134 /// registers to `frame_from + frame_vals.len()` restoring the callee's saved window, restore the
135 /// iterator stack, and undo the return-value write (`rv_slot`←`rv_old`) and the argument-window
136 /// clear (`arg_lo`..←`arg_old`) — leaving the machine exactly in the callee's pre-return state so
137 /// the callee body can be walked backward.
138 ReturnUndo {
139 frame_from: usize,
140 frame_vals: Vec<Value>,
141 iter_from: usize,
142 iter_vals: Vec<(Vec<Value>, usize)>,
143 rv_slot: usize,
144 rv_old: Value,
145 arg_lo: usize,
146 arg_old: Vec<Value>,
147 },
148}
149
150impl JournalEntry {
151 /// The erased-DATA cost of this entry in bytes — the Landauer energy proxy. Control/derivable
152 /// entries cost 0; a value-overwrite costs the machine word it displaced.
153 fn erased_bytes(&self) -> usize {
154 use std::mem::size_of;
155 match self {
156 JournalEntry::RegOverwrite { .. }
157 | JournalEntry::CollectionMutate { .. }
158 | JournalEntry::CrdtInterior { .. }
159 | JournalEntry::CrdtFieldInterior { .. } => size_of::<Value>(),
160 // Divisor + remainder are held for a self-contained reverse (peak-tape cost).
161 JournalEntry::DivReconstruct { .. } => 2 * size_of::<Value>(),
162 // A shift/mask entry holds one saved machine word; a modulo entry holds quotient + divisor.
163 JournalEntry::ShiftReconstruct { .. } | JournalEntry::MaskReconstruct { .. } => {
164 size_of::<Value>()
165 }
166 JournalEntry::ModReconstruct { .. } => 2 * size_of::<Value>(),
167 JournalEntry::ListPop { .. } => 2 * size_of::<Value>(),
168 JournalEntry::IterNextUndo { advanced, .. } => {
169 usize::from(*advanced) * size_of::<Value>()
170 }
171 JournalEntry::IterPopUndo { items, .. } => items.len() * size_of::<Value>(),
172 JournalEntry::CallUndo { .. } => 0,
173 JournalEntry::CallValueUndo { captures, .. } => captures.len() * size_of::<Value>(),
174 JournalEntry::ReturnUndo {
175 frame_vals,
176 arg_old,
177 ..
178 } => (frame_vals.len() + arg_old.len() + 1) * size_of::<Value>(),
179 JournalEntry::GlobalOverwrite { old, .. } => {
180 old.as_ref().map_or(0, |_| size_of::<Value>())
181 }
182 JournalEntry::MultiRegOverwrite { slots } => slots.len() * size_of::<Value>(),
183 // Only the overwritten register is erased data; `drawn` is captured INPUT entropy.
184 JournalEntry::NondetRead { .. } => size_of::<Value>(),
185 JournalEntry::ShowLine | JournalEntry::Branch { .. } => 0,
186 }
187 }
188
189 /// The GENUINELY-irreversible information this entry records — the true Landauer cost. Refines
190 /// [`JournalEntry::erased_bytes`]: a write that overwrote the DEFAULT (`Nothing`) destroyed no
191 /// information (the pristine initial state is known, so reverse re-derives it — the same reason
192 /// `GlobalOverwrite { old: None }` is free), and so costs 0. What remains is the information the
193 /// program truly erased — an overwrite of a live value, a lattice merge, a captured entropy read.
194 fn landauer_bytes(&self) -> usize {
195 use std::mem::size_of;
196 let w = size_of::<Value>();
197 let cost = |v: &Value| if v.is_nothing() { 0 } else { w };
198 match self {
199 JournalEntry::RegOverwrite { old, .. } | JournalEntry::CollectionMutate { old, .. } => {
200 cost(old)
201 }
202 // A CRDT interior snapshot records a join-semilattice state the merge/resolve destroyed —
203 // genuinely irreducible information.
204 JournalEntry::CrdtInterior { .. } | JournalEntry::CrdtFieldInterior { .. } => w,
205 // Only the remainder is erased (the divisor is a recomputable input, kept quotient aside).
206 // A remainder is `< |b|`, so it fits in `⌈log₂|b|⌉` bits — the true Landauer cost.
207 JournalEntry::DivReconstruct { divisor, .. } => {
208 let d = divisor.as_int().unwrap_or(0).unsigned_abs();
209 let bits = if d <= 1 {
210 0
211 } else {
212 (u64::BITS - (d - 1).leading_zeros()) as usize
213 };
214 bits.div_ceil(8)
215 }
216 // A shift destroys exactly the `k` shifted-out bits — the reconstruction recovers the rest
217 // from the kept result, so that is the whole erased-information cost.
218 JournalEntry::ShiftReconstruct { k, .. } => (*k as usize).div_ceil(8),
219 // A mask destroys the positions the live mask can't recover — `64 − popcount` for AND (the
220 // cleared bits), `popcount` for OR (the forced bits).
221 JournalEntry::MaskReconstruct { mask, and, .. } => {
222 let m = *mask as u64;
223 let bits = if *and {
224 (u64::BITS - m.count_ones()) as usize
225 } else {
226 m.count_ones() as usize
227 };
228 bits.div_ceil(8)
229 }
230 // Only the quotient is erased (the divisor is a live recomputable input; the remainder is
231 // kept in the destination). It fits in `⌈log₂|q|⌉` bits — the true Landauer cost.
232 JournalEntry::ModReconstruct { quotient, .. } => {
233 let q = quotient.unsigned_abs();
234 let bits = if q == 0 {
235 0
236 } else {
237 (u64::BITS - q.leading_zeros()) as usize
238 };
239 bits.div_ceil(8)
240 }
241 JournalEntry::ListPop {
242 list_old, dst_old, ..
243 } => cost(list_old) + cost(dst_old),
244 JournalEntry::IterNextUndo {
245 advanced, dst_old, ..
246 } => {
247 if *advanced {
248 cost(dst_old)
249 } else {
250 0
251 }
252 }
253 JournalEntry::IterPopUndo { items, .. } => items.iter().map(cost).sum(),
254 JournalEntry::CallUndo { .. } => 0,
255 JournalEntry::CallValueUndo { captures, .. } => {
256 captures.iter().map(|(_, v)| cost(v)).sum()
257 }
258 JournalEntry::ReturnUndo {
259 frame_vals,
260 arg_old,
261 rv_old,
262 ..
263 } => {
264 frame_vals.iter().map(cost).sum::<usize>()
265 + arg_old.iter().map(cost).sum::<usize>()
266 + cost(rv_old)
267 }
268 JournalEntry::GlobalOverwrite { old, .. } => old.as_ref().map_or(0, cost),
269 JournalEntry::MultiRegOverwrite { slots } => slots.iter().map(|(_, v)| cost(v)).sum(),
270 // The overwritten dst is the erased part; `drawn` is captured INPUT entropy, not erased.
271 JournalEntry::NondetRead { dst_old, .. } => cost(dst_old),
272 JournalEntry::ShowLine | JournalEntry::Branch { .. } => 0,
273 }
274 }
275}
276
277/// The append-forward / pop-backward garbage tape of a recording forward run. TWO stacks:
278///
279/// • `entries` — the erased-DATA deltas (register/global overwrites, output lines). These are the
280/// Landauer garbage; [`HistoryTape::erased_bytes`]/[`HistoryTape::len`] count them, and they are
281/// EMPTY for an all-abelian run.
282/// • `steps` — the executed program-counter trace (one entry per executed op), the control
283/// skeleton. Reverse execution walks it backward to replay the exact executed path — branches
284/// and loops fall out for free (a loop is just the trace with repeated pcs). This is control
285/// overhead (re-derivable by re-running forward, so zero erased DATA and Bennett-uncomputable),
286/// NOT part of the erased-bit count.
287#[derive(Debug, Default)]
288pub struct HistoryTape {
289 entries: Vec<JournalEntry>,
290 /// The executed `(pc, base)` trace. `base` is the register-window base the op ran in — carried
291 /// so reverse execution restores the correct frame when walking back through a function call.
292 steps: Vec<(usize, usize)>,
293}
294
295impl HistoryTape {
296 /// A fresh, empty tape.
297 pub fn new() -> Self {
298 HistoryTape {
299 entries: Vec::new(),
300 steps: Vec::new(),
301 }
302 }
303
304 /// Append an erased-data delta (forward execution).
305 pub fn push(&mut self, entry: JournalEntry) {
306 self.entries.push(entry);
307 }
308
309 /// Pop the most recent erased-data delta (reverse execution).
310 pub fn pop(&mut self) -> Option<JournalEntry> {
311 self.entries.pop()
312 }
313
314 /// Record that the op at `pc` (running in window `base`) executed (forward) — the control trace.
315 pub fn push_step(&mut self, pc: usize, base: usize) {
316 self.steps.push((pc, base));
317 }
318
319 /// Pop the most recently executed `(pc, base)` (reverse execution walks the path backward).
320 pub fn pop_step(&mut self) -> Option<(usize, usize)> {
321 self.steps.pop()
322 }
323
324 /// The number of recorded erased-data deltas.
325 pub fn len(&self) -> usize {
326 self.entries.len()
327 }
328
329 /// Whether the DATA tape is empty. `true` after an all-abelian (bijection-only) run — the
330 /// zero-erasure signature (a program may still have a non-empty control-flow `steps` trace).
331 pub fn is_empty(&self) -> bool {
332 self.entries.is_empty()
333 }
334
335 /// The number of executed ops on the control-flow trace.
336 pub fn step_count(&self) -> usize {
337 self.steps.len()
338 }
339
340 /// The number of recorded nondeterministic reads (clock / random) — the captured entropy count.
341 pub fn nondet_count(&self) -> usize {
342 self.entries
343 .iter()
344 .filter(|e| matches!(e, JournalEntry::NondetRead { .. }))
345 .count()
346 }
347
348 /// Total erased DATA on the tape, in bytes — the Landauer energy proxy. Exactly `0` for an
349 /// all-abelian run (every op self-inverts, nothing is journaled), regardless of how many times a
350 /// reversible loop body ran.
351 pub fn erased_bytes(&self) -> usize {
352 self.entries.iter().map(JournalEntry::erased_bytes).sum()
353 }
354
355 /// The GENUINELY-irreversible information on the tape — the true Landauer (energy) number. Below
356 /// [`HistoryTape::erased_bytes`] because a write that overwrote the DEFAULT (`Nothing`) destroyed
357 /// no information: pebbling elides the recomputable scratch, and this excludes the zero-information
358 /// first-writes, so a pure computation into an output measures **0**.
359 pub fn landauer_bytes(&self) -> usize {
360 self.entries.iter().map(JournalEntry::landauer_bytes).sum()
361 }
362}
363
364#[cfg(test)]
365#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/vm/journal/tests.rs"]
366mod tests;