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}