Skip to main content

logicaffeine_compile/vm/
reverse.rs

1//! Reversibility classification of the bytecode ISA — the static substrate for running the whole
2//! language BACKWARD (`forward ; reverse ≡ identity`).
3//!
4//! Because the register-VM opcode set is the chip ISA, a reversibility
5//! strategy for every op is the software prerequisite for adiabatic / sub-Landauer hardware that
6//! recycles charge instead of erasing it. Every
7//! instruction lands in exactly one of four working strategies (or the empty `NonReversible`
8//! census), and [`reversibility`] is EXHAUSTIVE on `Op` with NO wildcard arm — a new instruction
9//! cannot compile until it is classified, so reversibility can never be silently broken. The
10//! `tests/reverse_op_lock.rs` catalog lock enforces the census, the monotone zero-garbage floor,
11//! and inverse well-formedness.
12//!
13//! This module is the STATIC half. The behavioural half — a reverse executor proving
14//! `forward ; reverse` restores the exact initial state, with the history tape at zero bytes for
15//! the abelian core — lands in R1+ (`vm::journal`, `Vm::run_reverse`).
16
17use crate::vm::instruction::{Op, Reg};
18
19/// How an op participates in reversible execution. Four working strategies plus a census bucket
20/// that MUST stay empty. `Copy` so a lock can classify every variant without allocation.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub enum RevClass {
23    /// Injective on machine state with a named inverse — reverses with ZERO garbage (the abelian /
24    /// involution core; the adiabatic charge-recovery path). The zero-erasure ideal.
25    Bijection(InverseSpec),
26    /// Information-losing — reversed by a minimal Bennett delta appended to the history tape
27    /// ([`JournalKind`] names WHAT the delta records). Correct, but pays garbage until uncomputed.
28    Journaled(JournalKind),
29    /// An observable side effect — reversed via the effect-log and, where the world cannot be
30    /// recalled, a NAMED irreversibility barrier ([`EffectKind`] names the effect family).
31    Effectful(EffectKind),
32    /// Control flow or a value-preserving instruction — reversed on the recorded execution trace
33    /// (branch/loop/call decisions), with no data inverse and no journal entry.
34    Control,
35    /// GENUINE census of ops with no reversibility story yet. This MUST stay ∅ — see the lock.
36    NonReversible(&'static str),
37}
38
39/// How a [`RevClass::Bijection`] op inverts — the zero-garbage inverse recipe.
40#[derive(Clone, Copy, PartialEq, Eq, Debug)]
41pub enum InverseSpec {
42    /// The op is its own inverse: applying it twice is the identity (`Not`-into-self, `Swap`,
43    /// additive negation, XOR-by-a-value). `f ∘ f = id`.
44    Involution,
45    /// The inverse is the SAME op variant with a flag flipped (e.g. `CrdtBump { negate }` inverts
46    /// by flipping `negate`). The variant carries its own inverse direction.
47    SelfFamily,
48    /// The inverse is a DISTINCT op variant, named by its [`Op::name`] (e.g. `RevAdd`⁻¹ = `RevSub`).
49    /// The lock checks the name resolves to a real ISA op.
50    NamedOp(&'static str),
51}
52
53/// What a [`RevClass::Journaled`] op must record on the history tape to be reversible — the minimal
54/// destroyed information (Bennett/Landauer embedding), not a full state snapshot.
55#[derive(Clone, Copy, PartialEq, Eq, Debug)]
56pub enum JournalKind {
57    /// Writes a destination register/global that is NOT one of its operands: the operands survive,
58    /// so reversal only needs the destination's PRIOR value (one entry). Covers the bulk of the
59    /// three-address ISA — arithmetic, comparisons, constructors, loads, field reads.
60    Overwrite,
61    /// Writes a destination that IS one of its operands (`x := x ⊕ e`): the prior value is lost.
62    /// Reversible algebraically when `⊕` is an abelian group op (the R1 promotion to `Bijection`),
63    /// or by journaling the prior value.
64    InPlace,
65    /// Mutates a heap collection in place (push / pop / remove / index-store / struct-field write):
66    /// reversal records the structural delta (the pushed slot to pop, the popped element + index to
67    /// re-insert, the overwritten slot's old value).
68    Collection,
69    /// A CRDT join-semilattice step (merge / resolve) that collapses lattice points and cannot be
70    /// inverted algebraically: reversal snapshots the target replica's pre-state (bounded to that
71    /// one value, not the whole machine).
72    CrdtSnapshot,
73}
74
75/// The family of observable effect a [`RevClass::Effectful`] op performs — determines how far
76/// reversal can go before it hits a named barrier.
77#[derive(Clone, Copy, PartialEq, Eq, Debug)]
78pub enum EffectKind {
79    /// Buffered program output (`Show`) — fully reversible by truncating the output buffer.
80    Output,
81    /// Reads the environment clock (`today` / `now`) — reversed via the value recorded on the
82    /// determinism trace, so a reverse-then-re-forward replays the identical instant.
83    Clock,
84    /// Advances virtual time (`Sleep`) — a no-op on observable state; reverses as a no-op.
85    Timer,
86    /// Deterministic in-process concurrency (channels / tasks / select) — internal queue and
87    /// scheduler mutations, reversed via the recorded schedule trace.
88    Concurrency,
89    /// Peer networking over the relay — the genuinely external frontier; internal state reverses
90    /// fully, but an already-observed send raises a NAMED irreversibility barrier.
91    Network,
92}
93
94/// The reversibility fidelity ⇄ space ⇄ time point a recording run is asked to hit — the pebbling
95/// knob (`vm/pebble.rs`). The tape is only as large as the mode demands.
96#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
97pub enum ReverseMode {
98    /// Restore the program-OBSERVABLE state (named variables, globals, output, iterators); compiler
99    /// scratch is program-invisible, so its journal is elided. Smallest tape, fastest. The default.
100    #[default]
101    Observable,
102    /// Restore EVERY register bit-exactly by journaling all destroyed values. Largest tape, simplest.
103    FullState,
104    /// Restore every register bit-exactly at the LANDAUER FLOOR: recompute pure scratch on reverse
105    /// (Bennett uncompute) instead of journaling it. Full fidelity, minimal tape, +recompute time —
106    /// the adiabatic chip contract.
107    Adiabatic,
108}
109
110/// The registers a REVERSE step reads from the live machine (not from the tape) when undoing `op` —
111/// so their values must be intact at reverse time. Journaled overwrites restore their destination
112/// straight from the tape and read NOTHING live, so they contribute none; only the `Bijection` ops
113/// (which recompute in place) and the collection/CRDT reverses (which read the live handle) do.
114/// Eliding a journal entry for a register in this set could corrupt a later reverse-read, so the
115/// pebbler treats membership here as "must keep". Keep in lockstep with the reverse executor.
116pub fn reverse_reads(op: &Op) -> Vec<Reg> {
117    match op {
118        Op::RevAdd { place, delta } | Op::RevSub { place, delta } | Op::RevXor { place, delta } => {
119            vec![*place, *delta]
120        }
121        Op::RevNeg { place } => vec![*place],
122        Op::RevSwap { a, b } => vec![*a, *b],
123        // An in-place division reverses by reconstructing `a_old = q·b + r`, reading the kept quotient
124        // straight from `dst` at reverse time — so `dst` must not be elided out from under it.
125        Op::Div { dst, lhs, .. } if dst == lhs => vec![*dst],
126        // In-place shifts / masks / modulo likewise reconstruct `a_old` from the KEPT result in `dst`
127        // (plus bits/quotient held self-contained on the tape), so `dst` must survive elision.
128        Op::Shr { dst, lhs, .. } | Op::Shl { dst, lhs, .. } if dst == lhs => vec![*dst],
129        Op::DivPow2 { dst, lhs, .. } if dst == lhs => vec![*dst],
130        Op::BitAnd { dst, lhs, .. } | Op::BitOr { dst, lhs, .. } if dst == lhs => vec![*dst],
131        Op::Mod { dst, lhs, .. } if dst == lhs => vec![*dst],
132        // `ListPop`/`CrdtAppend`/`CrdtResolve` and every `mutated_collection` op reverse THROUGH the
133        // live handle (`reverse_collection_mutate` reads it for the aliasing check), so that register
134        // must survive.
135        Op::ListPop { list, .. } => vec![*list],
136        Op::CrdtAppend { seq, .. } => vec![*seq],
137        Op::CrdtResolve { obj, .. } => vec![*obj],
138        _ => mutated_collection_reg(op).into_iter().collect(),
139    }
140}
141
142/// ★ THE STATIC REVERSIBILITY CATALOG ★ — the single source of truth for how every VM bytecode op
143/// is run backward. EXHAUSTIVE on `Op` (no `_` arm): adding an instruction fails to compile until it
144/// is classified, so no op can silently escape a reversibility strategy. Keep this in lockstep with
145/// the reverse executor (`vm::journal` / `Vm::run_reverse`, R1+) — the behavioural roundtrip lock
146/// proves they agree.
147///
148/// R0 ships the classification only; the abelian promotions (`Journaled(InPlace)` → `Bijection`,
149/// e.g. `CrdtBump`) land in R1 alongside the reverse executor that proves each roundtrip.
150pub fn reversibility(op: &Op) -> RevClass {
151    use EffectKind as E;
152    use InverseSpec::{Involution, NamedOp};
153    use JournalKind::{Collection, CrdtSnapshot, InPlace, Overwrite};
154    use RevClass::{Bijection, Control, Effectful, Journaled};
155
156    match op {
157        // ── Abelian / involution core — ZERO garbage: self-inverts / algebraic inverse, no tape ──
158        // Swapping two registers is its own inverse; reverse execution re-applies it.
159        Op::RevSwap { .. } => Bijection(Involution),
160        // In-place XOR over ℤ/2ⁿ is self-inverse (`x ⊕ e ⊕ e = x`); reverse re-applies it.
161        Op::RevXor { .. } => Bijection(Involution),
162        // Modular negation is self-inverse (`-(-x) = x`).
163        Op::RevNeg { .. } => Bijection(Involution),
164        // The (ℤ/2ⁿ, +) additive translations: RevAdd and RevSub are MUTUAL inverses.
165        Op::RevAdd { .. } => Bijection(NamedOp("RevSub")),
166        Op::RevSub { .. } => Bijection(NamedOp("RevAdd")),
167
168        // ── Value-preserving / control flow — reversed on the recorded trace, no data inverse ──
169        // A copy-on-write ownership barrier leaves the observed value unchanged.
170        Op::EnsureOwned { .. } => Control,
171        // A native-region bounds hoist is a pure no-op outside the JIT.
172        Op::RegionBoundsGuard { .. } => Control,
173        // Branches carry no data mutation; the taken direction is recorded on the trace.
174        Op::Jump { .. } | Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. } => Control,
175        // Calls / returns reverse via the frame trace (CallEnter/Leave, R2) + callee uncompute.
176        Op::Call { .. } | Op::CallValue { .. } | Op::Return { .. } | Op::ReturnNothing => Control,
177        // `Check …` traps on failure and is otherwise a no-op on state.
178        Op::CheckPolicy { .. } => Control,
179        // Snapshot-iterator bookkeeping (the iterator stack) is control state, reversed via the
180        // Iter trace in R2.
181        Op::IterPrepare { .. } | Op::IterNext { .. } | Op::IterPop => Control,
182        // A terminal error / stop: nothing to reverse past it.
183        Op::FailWith { .. } | Op::Halt => Control,
184
185        // ── Overwrite: writes a fresh destination; operands survive → journal the old dst ──
186        Op::LoadConst { .. } | Op::Move { .. } => Journaled(Overwrite),
187        Op::Add { .. } | Op::Sub { .. } | Op::Mul { .. } | Op::Pow { .. } => Journaled(Overwrite),
188        Op::BigIntWide { .. } => Journaled(Overwrite),
189        Op::Div { .. } | Op::ExactDiv { .. } | Op::FloorDiv { .. } | Op::Mod { .. } => {
190            Journaled(Overwrite)
191        }
192        Op::DivPow2 { .. } | Op::MagicDivU { .. } => Journaled(Overwrite),
193        Op::Lt { .. } | Op::Gt { .. } | Op::LtEq { .. } | Op::GtEq { .. } => Journaled(Overwrite),
194        Op::Eq { .. } | Op::NotEq { .. } | Op::ApproxEq { .. } => Journaled(Overwrite),
195        Op::Not { .. } => Journaled(Overwrite),
196        Op::BitXor { .. } | Op::BitAnd { .. } | Op::BitOr { .. } => Journaled(Overwrite),
197        Op::Shl { .. } | Op::Shr { .. } => Journaled(Overwrite),
198        Op::Concat { .. } | Op::SeqConcat { .. } | Op::FormatValue { .. } => Journaled(Overwrite),
199        Op::SliceOp { .. } | Op::DeepClone { .. } => Journaled(Overwrite),
200        Op::UnionOp { .. } | Op::IntersectOp { .. } => Journaled(Overwrite),
201        Op::Length { .. } | Op::Contains { .. } => Journaled(Overwrite),
202        Op::Index { .. } | Op::IndexUnchecked { .. } | Op::GetField { .. } => Journaled(Overwrite),
203        Op::NewList { .. }
204        | Op::NewEmptyList { .. }
205        | Op::NewEmptyListWithCapacity { .. }
206        | Op::NewEmptyListFixed { .. }
207        | Op::NewEmptyListI32 { .. } => Journaled(Overwrite),
208        Op::NewEmptySet { .. }
209        | Op::NewEmptyMap { .. }
210        | Op::NewEmptyMapWithCapacity { .. }
211        | Op::NewRange { .. } => Journaled(Overwrite),
212        Op::NewTuple { .. } | Op::DestructureTuple { .. } => Journaled(Overwrite),
213        Op::NewStruct { .. } | Op::NewInductive { .. } | Op::NewCrdt { .. } => Journaled(Overwrite),
214        Op::TestArm { .. } | Op::BindArm { .. } => Journaled(Overwrite),
215        Op::GlobalGet { .. } | Op::GlobalSet { .. } => Journaled(Overwrite),
216        Op::CallBuiltin { .. } => Journaled(Overwrite),
217        Op::MakeClosure { .. } => Journaled(Overwrite),
218        // `args()` reads the run's argv (fixed per run) into a fresh dst; reversal restores old dst.
219        Op::Args { .. } => Journaled(Overwrite),
220        // A `PeerAgent` handle is the address itself — pure, into a fresh dst.
221        Op::NetMakePeer { .. } => Journaled(Overwrite),
222
223        // ── In-place scalar (dst is an operand) — abelian, promoted to Bijection in R1 ──
224        // `Set x to x + …` (amortized append form); `Increase/Decrease` counter bump.
225        Op::AddAssign { .. } => Journaled(InPlace),
226        Op::CrdtBump { .. } => Journaled(InPlace),
227
228        // ── Collection: in-place heap mutation — journal the structural delta ──
229        Op::ListPush { .. } | Op::ListPushField { .. } | Op::SetAdd { .. } => Journaled(Collection),
230        Op::CrdtAppend { .. } => Journaled(Collection),
231        Op::ListPop { .. } | Op::RemoveFrom { .. } => Journaled(Collection),
232        Op::SetIndex { .. } | Op::SetIndexUnchecked { .. } => Journaled(Collection),
233        Op::StructInsert { .. } => Journaled(Collection),
234
235        // ── CRDT join-semilattice — lossy merge/resolve, snapshot the pre-state ──
236        Op::CrdtMerge { .. } | Op::CrdtJoin { .. } | Op::CrdtResolve { .. } => {
237            Journaled(CrdtSnapshot)
238        }
239
240        // ── Effectful — reversed via the effect-log / determinism trace / a named barrier ──
241        Op::Show { .. } => Effectful(E::Output),
242        Op::LoadToday { .. } | Op::LoadNow { .. } => Effectful(E::Clock),
243        Op::Sleep { .. } => Effectful(E::Timer),
244        Op::ChanNew { .. }
245        | Op::ChanSend { .. }
246        | Op::ChanRecv { .. }
247        | Op::ChanTrySend { .. }
248        | Op::ChanTryRecv { .. }
249        | Op::ChanClose { .. } => Effectful(E::Concurrency),
250        Op::Spawn { .. } | Op::SpawnHandle { .. } | Op::TaskAwait { .. } | Op::TaskAbort { .. } => {
251            Effectful(E::Concurrency)
252        }
253        Op::SelectArmRecv { .. } | Op::SelectArmTimeout { .. } | Op::SelectWait { .. } => {
254            Effectful(E::Concurrency)
255        }
256        Op::NetConnect { .. }
257        | Op::NetListen { .. }
258        | Op::NetSend { .. }
259        | Op::NetStream { .. }
260        | Op::NetAwait { .. }
261        | Op::NetSync { .. }
262        | Op::Tcp { .. }
263        | Op::Serve { .. } => Effectful(E::Network),
264
265        // Observation (Wave 13) writes only to the observation channel — an
266        // output-class effect, reversed via the effect log exactly like `Show`.
267        Op::Obs { .. } => Effectful(E::Output),
268    }
269}
270
271/// The single register an op OVERWRITES with a fresh value — its data destination — for the ops
272/// whose only state effect is that write and whose operands survive. Reverse execution restores this
273/// register's prior value from the tape. `None` for ops that write a global / heap object / output
274/// line instead (handled separately) or write nothing.
275///
276/// This is CORRECTNESS-GRADE and deliberately distinct from [`crate::vm::disasm::op_io`] (a debug
277/// display helper whose lossy `_ => default` wildcard would silently report "no write" for an
278/// unenumerated op — a reverse-execution landmine). Every op here is one the reverse executor is
279/// proven to restore.
280pub fn overwritten_reg(op: &Op) -> Option<Reg> {
281    match op {
282        Op::LoadConst { dst, .. }
283        | Op::Move { dst, .. }
284        | Op::GlobalGet { dst, .. }
285        | Op::Add { dst, .. }
286        | Op::Sub { dst, .. }
287        | Op::Mul { dst, .. }
288        | Op::Div { dst, .. }
289        | Op::ExactDiv { dst, .. }
290        | Op::FloorDiv { dst, .. }
291        | Op::Mod { dst, .. }
292        | Op::DivPow2 { dst, .. }
293        | Op::MagicDivU { dst, .. }
294        | Op::Lt { dst, .. }
295        | Op::Gt { dst, .. }
296        | Op::LtEq { dst, .. }
297        | Op::GtEq { dst, .. }
298        | Op::Eq { dst, .. }
299        | Op::NotEq { dst, .. }
300        | Op::ApproxEq { dst, .. }
301        | Op::Not { dst, .. }
302        | Op::Concat { dst, .. }
303        | Op::SeqConcat { dst, .. }
304        | Op::Pow { dst, .. }
305        | Op::BitXor { dst, .. }
306        | Op::BitAnd { dst, .. }
307        | Op::BitOr { dst, .. }
308        | Op::Shl { dst, .. }
309        | Op::Shr { dst, .. }
310        | Op::AddAssign { dst, .. }
311        // Pure READS into a fresh dst — the source collection/struct is not mutated, so restoring
312        // dst's prior value is an exact reversal (any freshly-aliased Rc drops back to its old count).
313        | Op::Index { dst, .. }
314        | Op::IndexUnchecked { dst, .. }
315        | Op::Length { dst, .. }
316        | Op::Contains { dst, .. }
317        | Op::SliceOp { dst, .. }
318        | Op::DeepClone { dst, .. }
319        | Op::FormatValue { dst, .. }
320        | Op::UnionOp { dst, .. }
321        | Op::IntersectOp { dst, .. }
322        | Op::GetField { dst, .. }
323        | Op::TestArm { dst, .. }
324        | Op::BindArm { dst, .. }
325        // Pure CONSTRUCTION into a fresh dst — a brand-new heap object with no prior aliases, so
326        // restoring dst (and dropping the new object) is exact. `NewCrdt` is EXCLUDED (it advances a
327        // global replica-id counter — a side effect the store snapshot can't reverse).
328        | Op::NewStruct { dst, .. }
329        | Op::NewInductive { dst, .. }
330        | Op::NewList { dst, .. }
331        | Op::NewEmptyList { dst, .. }
332        | Op::NewEmptyListWithCapacity { dst, .. }
333        | Op::NewEmptyListFixed { dst, .. }
334        | Op::NewEmptyListI32 { dst, .. }
335        | Op::NewEmptySet { dst, .. }
336        | Op::NewEmptyMap { dst, .. }
337        | Op::NewEmptyMapWithCapacity { dst, .. }
338        | Op::NewRange { dst, .. }
339        | Op::NewTuple { dst, .. }
340        // A closure snapshots its captures (deep-cloned) into a fresh dst — a pure constructor.
341        | Op::MakeClosure { dst, .. }
342        // `args()` reads the run's argv into a fresh dst; a `PeerAgent` handle is the address itself.
343        | Op::Args { dst, .. }
344        | Op::NetMakePeer { dst, .. } => Some(*dst),
345        // A copy-on-write barrier rewrites its register (or is a no-op); restoring the prior handle
346        // is exact (journaling it forces the clone, leaving the original pristine).
347        Op::EnsureOwned { reg } => Some(*reg),
348        _ => None,
349    }
350}
351
352/// Whether forward execution RECORDS a history-tape delta for `op` — i.e. it destroys data a
353/// reverse pass must restore. Exactly the ops that write a global/output line or a scalar register.
354/// The DUAL of the reverse pass's pop condition: `record_op` pushes iff this is true, and
355/// `run_reverse` pops iff this is true, so the tape stays aligned. A [`RevClass::Bijection`] op is
356/// never here — it self-inverts with no tape entry (the zero-garbage core).
357/// The single collection register an op mutates IN PLACE (push / index-set / add / remove) — for the
358/// ops reversed by restoring the pristine pre-mutation snapshot ([`super::journal::JournalEntry::CollectionMutate`]).
359/// `None` for non-mutating ops and for `ListPop` (which ALSO writes a dst — it carries its own
360/// combined entry). Journaling this register's value forces the op's copy-on-write, so the original
361/// stays pristine for the reverse.
362pub fn mutated_collection_reg(op: &Op) -> Option<Reg> {
363    match op {
364        Op::ListPush { list, .. } => Some(*list),
365        Op::SetAdd { set, .. } => Some(*set),
366        Op::RemoveFrom { collection, .. } => Some(*collection),
367        Op::SetIndex { collection, .. } | Op::SetIndexUnchecked { collection, .. } => {
368            Some(*collection)
369        }
370        // A struct is a by-value `Box<StructValue>`: the pre-insert snapshot is a deep clone (the
371        // replaced field's old value is captured), so restoring it is exact. (`ListPushField` is
372        // EXCLUDED — it mutates a nested Rc list, which a struct clone would share, not snapshot.)
373        Op::StructInsert { obj, .. } => Some(*obj),
374        // A single-replica counter bump mutates a by-value struct's Int field — same deep-clone
375        // snapshot as `StructInsert`.
376        Op::CrdtBump { obj, .. } => Some(*obj),
377        // A CRDT merge folds the source's fields into the target struct by REPLACING field values
378        // with freshly-computed joins (`crdt_merge_field` is pure — it never mutates a nested `Rc`
379        // in place), so a deep-clone snapshot of the target is exact, like `StructInsert`.
380        Op::CrdtMerge { target, .. } | Op::CrdtJoin { target, .. } => Some(*target),
381        Op::BigIntWide { dst, .. } => Some(*dst),
382        _ => None,
383    }
384}
385
386/// Whether a builtin is PURE — a deterministic function of its arguments, reversible by restoring the
387/// destination and the cleared argument registers. Excludes the wire-subsystem builtins, which touch
388/// external sinks or run shipped code (effects the store snapshot cannot reverse).
389pub fn reversible_builtin(b: crate::semantics::builtins::BuiltinId) -> bool {
390    use crate::semantics::builtins::BuiltinId;
391    !matches!(
392        b,
393        BuiltinId::WireBytes
394            | BuiltinId::ReadWireProgram
395            | BuiltinId::WriteWireResidual
396            | BuiltinId::RunAccepted
397    )
398}
399
400pub fn records(op: &Op) -> bool {
401    matches!(op, Op::GlobalSet { .. } | Op::Show { .. } | Op::ListPop { .. } | Op::DestructureTuple { .. })
402        // CRDT interior mutations journal a deep pre-mutation snapshot of the shared handle.
403        || matches!(op, Op::CrdtAppend { .. } | Op::CrdtResolve { .. })
404        || matches!(op, Op::CallBuiltin { builtin, .. } if reversible_builtin(*builtin))
405        || mutated_collection_reg(op).is_some()
406        || overwritten_reg(op).is_some()
407}
408
409/// Whether the reverse executor supports `op` yet — the GROWING reversible subset. R1 covers the
410/// abelian core (`RevSwap`, zero garbage), straight-line scalar compute ([`overwritten_reg`]), the
411/// global write/read, output, and program stop. A program containing any other op declines recording
412/// with a clear error rather than silently mis-reversing; the subset expands (control flow,
413/// collections, effects) in R2/R3.
414/// If `op` is a truly-EXTERNAL effect that cannot be un-done — the honest Landauer wall, an
415/// already-sent network message can't be un-sent — name the irreversibility barrier. `None` for
416/// reversible ops and for internally-modeled effects (buffered output, in-process channels, the
417/// clock) that DO reverse. This turns an irreversibility into a NAMED, enumerable boundary instead of
418/// a generic "unsupported", so the reverse executor never silently pretends the world can be rewound.
419pub fn external_effect_barrier(op: &Op) -> Option<&'static str> {
420    matches!(reversibility(op), RevClass::Effectful(EffectKind::Network)).then_some("network")
421}
422
423pub fn is_reversible_supported(op: &Op) -> bool {
424    matches!(
425        op,
426        Op::Halt
427            | Op::GlobalSet { .. }
428            | Op::Show { .. }
429            // Control flow — mutates only `pc`, reconstructed from the recorded step trace.
430            | Op::Jump { .. }
431            | Op::JumpIfFalse { .. }
432            | Op::JumpIfTrue { .. }
433            // Interpreter function calls — reversed by the CallUndo / ReturnUndo frame journal.
434            | Op::Call { .. }
435            | Op::CallValue { .. }
436            | Op::Return { .. }
437            | Op::ReturnNothing
438            // `Repeat` iterator machinery — reversed by reconstructing the iterator stack.
439            | Op::IterPrepare { .. }
440            | Op::IterNext { .. }
441            | Op::IterPop
442            // Nondeterministic clock reads — reversed by dst-restore; the drawn instant is captured.
443            | Op::LoadNow { .. }
444            | Op::LoadToday { .. }
445            // Abelian zero-garbage core.
446            | Op::RevSwap { .. }
447            | Op::RevXor { .. }
448            | Op::RevNeg { .. }
449            | Op::RevAdd { .. }
450            | Op::RevSub { .. }
451            // CRDT create + interior mutation. `NewCrdt` draws a replica id from a global counter —
452            // captured nondeterminism (the clock pattern): reversed by dst-restore, the counter left
453            // advanced. `CrdtAppend` mutates the shared `Rc<RefCell<CrdtValue>>` interior in place;
454            // reversed by a deep interior snapshot restored through the live handle.
455            | Op::NewCrdt { .. }
456            | Op::CrdtAppend { .. }
457            // A resolve onto a nested CRDT register field is a deep field-interior snapshot; onto a
458            // plain field it is a Box snapshot (both from the `records`/reverse `CrdtResolve` path).
459            | Op::CrdtResolve { .. }
460            // In-place collection mutation that also writes a dst.
461            | Op::ListPop { .. }
462            // Tuple destructure (multi-register write) + a pure interpreter no-op guard.
463            | Op::DestructureTuple { .. }
464            | Op::RegionBoundsGuard { .. }
465            // A Sleep advances virtual time only — no observable state change, reverses to nothing.
466            | Op::Sleep { .. }
467            // Fire-and-forget spawn — the child is a separate task VM the reversible scheduler
468            // reverses on its own tape; the spawn op itself writes nothing, reversing to nothing.
469            | Op::Spawn { .. }
470            // Spawn-with-handle delivers a TaskHandle; Await joins a child, taking its result. Both
471            // are reversed by the scheduler (handle / result restored from the recorded schedule).
472            | Op::SpawnHandle { .. }
473            | Op::TaskAwait { .. }
474            // Channel create / send / receive — the op itself only suspends (writes nothing on the
475            // task tape); the reversible scheduler undoes the channel delta + the delivered resume
476            // value from the recorded schedule.
477            | Op::ChanNew { .. }
478            | Op::ChanSend { .. }
479            | Op::ChanRecv { .. }
480            // Non-blocking channel ops: TrySend (always succeeds on an unbounded channel — Bool(true)),
481            // TryRecv (value-or-Nothing), Close (a reversible closed flag). All reversed by the
482            // scheduler from the recorded resume + channel delta.
483            | Op::ChanTrySend { .. }
484            | Op::ChanTryRecv { .. }
485            | Op::ChanClose { .. }
486            // Select — the arm-registration ops accumulate `select_pending` (writing nothing on the
487            // tape); `SelectWait` suspends, and the scheduler undoes the winning arm's value + index
488            // deliveries and any channel dequeue from the recorded schedule.
489            | Op::SelectArmRecv { .. }
490            | Op::SelectArmTimeout { .. }
491            | Op::SelectWait { .. }
492    ) || matches!(op, Op::CallBuiltin { builtin, .. } if reversible_builtin(*builtin))
493        || mutated_collection_reg(op).is_some()
494        || overwritten_reg(op).is_some()
495}