Skip to content

Commit fd3f672

Browse files
hyperpolymathclaude
andcommitted
feat(jtv): v2 reversibility C1 Phase 1 — execute_inverse + 7 RV tests
`reverse { x += v }` now correctly implements subtraction per the canonical v2 design (docs/language/DESIGN-JTV-V2-REVERSIBILITY.md): subtraction is not a grammar primitive, it arises from reversing addition. Before: eval_reverse_block called execute_forward → x = x + v (WRONG) After: eval_reverse_block calls execute_inverse → x = x - v (correct) New ReversibleInterpreter.execute_inverse: - Applies the INVERSE of each operation in reverse declaration order - AddAssign → SubAssign, SubAssign → AddAssign - Multi-op blocks invert in reverse order for mathematical correctness - If branches re-evaluate condition on current state (Janus requirement) The CNO (Certified Null Operation) pattern (execute_and_reverse) is unchanged: execute_forward then execute_reverse still gives identity. This is used by the future `reversible { } -> tok; reverse tok` construct (Phase C2). 7 tests (RV1–RV7): single-op, chain, cross-var, CNO, full-program, left-inverse. All 541 existing jtv-core tests remain green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b1acb63 commit fd3f672

4 files changed

Lines changed: 259 additions & 22 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ milestones = [
2929
"LSP server — DONE (tower-lsp integration)",
3030
"coprocessor support design — DONE (ADRs 0001-0006, 13 decisions closed 2026-04-19)",
3131
"coprocessor support implementation — DONE (Phase 2+4, 2026-04-27)",
32-
"v2 reversibility implementation — PENDING (5 decisions closed, C1 Phase 1 impl pending)"
32+
"v2 reversibility C1 Phase 1 — COMPLETE (2026-04-27): execute_inverse added; reverse { x += v } = subtraction; 7 RV tests"
3333
]
3434

3535
[blockers-and-issues]
@@ -40,7 +40,7 @@ issues = [
4040
[critical-next-actions]
4141
actions = [
4242
"Zig FFI + Idris2 ABI lowering for live coproc decls (unblocks ExternCoprocNotYetLowered at call sites)",
43-
"Start v2 reversibility C1 Phase 1 implementation (7-step plan in 007-jtv-v2-design-decisions memory)",
43+
"v2 reversibility C2: add `reversible { } -> tok` / `reverse tok` / `abandon tok` grammar + ReversalToken value",
4444
"Review WASM backend for extern-coproc lowering (now unblocked)"
4545
]
4646

@@ -53,7 +53,8 @@ sessions = [
5353
{ date = "2026-04-11", subject = "6a2 files converted from scheme to A2ML" },
5454
{ date = "2026-04-12", subject = "v2 reversibility design — 5 decisions closed" },
5555
{ date = "2026-04-19", subject = "coprocessor pata pathway designed — ADRs 0001-0005 + 0006 (Delta pivot to PataCL)" },
56-
{ date = "2026-04-27", subject = "PataCL Phases 1-4 complete; JtV Phase 2 landed (extern coproc grammar, CoprocNamespace, phase-boundary error, 12/12 coproc_tests.rs); conformance corpus CC1-CC7 (7/7); jtv-cli ExternCoproc fix" }
56+
{ date = "2026-04-27", subject = "PataCL Phases 1-4 complete; JtV Phase 2 landed (extern coproc grammar, CoprocNamespace, phase-boundary error, 12/12 coproc_tests.rs); conformance corpus CC1-CC7 (7/7); jtv-cli ExternCoproc fix" },
57+
{ date = "2026-04-27", subject = "Phase A: Zig FFI + Idris2 ABI lowering (coproc_lower.rs, 6 CL tests); Phase C: PataCL self-hosting (conformance/patacl-self.pata, 8 SH tests in patacl); Phase B: v2 reversibility C1 (execute_inverse added, reverse { } = subtraction, 7 RV tests)" }
5758
]
5859

5960
[design-artefact-locations]

crates/jtv-core/src/interpreter.rs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -378,35 +378,33 @@ impl Interpreter {
378378
}
379379

380380
fn eval_reverse_block(&mut self, block: &ReverseBlock) -> Result<()> {
381-
// Delegate to ReversibleInterpreter which properly records and
382-
// reverses operations. This implements JTV v2 semantics:
383-
// 1. Execute forward (recording operations)
384-
// 2. Execute reverse (inverting and reversing operations)
385-
// 3. Net effect: identity (CNO by construction)
381+
// JtV v2 semantics: `reverse { x += v }` IS subtraction.
382+
// Subtraction is not a grammar primitive; it arises from reversing addition.
386383
//
387-
// The forward pass modifies state. The reverse pass undoes it.
388-
// After execute_and_reverse, state returns to its original value.
384+
// `execute_inverse` applies the INVERSE of each operation in reverse
385+
// declaration order. For simple single-op blocks:
386+
// reverse { x += 5 } → x = x - 5
387+
// reverse { x -= 5 } → x = x + 5
389388
//
390-
// This is the core of the JTV v2 reversibility vision:
391-
// - Subtraction is NOT in the grammar
392-
// - Subtraction arises from reversing addition
393-
// - reverse { x += 5 } produces x -= 5 automatically
394-
// - forward ; reverse = CNO (Certified Null Operation)
389+
// For multi-op blocks, inversion happens in reverse order so that
390+
// the combined effect is the mathematical inverse of the forward block.
391+
//
392+
// The CNO (Certified Null Operation) pattern uses a SEPARATE `reversible`
393+
// block (Phase 2 — not yet in the grammar) for the forward pass, then
394+
// `reverse` to undo. The current `reverse { }` construct applies
395+
// inverses directly (no forward pass).
395396
use crate::reversible::ReversibleInterpreter;
396397

397398
let mut rev_interp = ReversibleInterpreter::with_state(self.globals.clone());
399+
rev_interp.execute_inverse(block)?;
398400

399-
// Forward execution (records operations for later reversal)
400-
rev_interp.execute_forward(block)?;
401-
402-
// Copy forward results back to main interpreter
403401
for (name, value) in rev_interp.get_state() {
404402
self.set_variable(name.clone(), value.clone());
405403
}
406404

407-
// Record trace for instrumentation (if enabled)
408405
if self.trace_enabled {
409-
self.add_trace("reverse_block_forward", &format!("executed {} reversible operations", block.body.len()));
406+
self.add_trace("reverse_block",
407+
&format!("applied inverse of {} operations", block.body.len()));
410408
}
411409

412410
Ok(())

crates/jtv-core/src/reversible.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,72 @@ impl ReversibleInterpreter {
120120
Ok(())
121121
}
122122

123-
/// Execute forward then reverse (should return to original state)
123+
/// Execute forward then reverse (should return to original state).
124+
/// This is the CNO (Certified Null Operation) pattern — the net effect
125+
/// on state is identity. Used when you want to prove reversibility.
124126
pub fn execute_and_reverse(&mut self, block: &ReverseBlock) -> Result<()> {
125127
self.execute_forward(block)?;
126128
self.execute_reverse()
127129
}
128130

131+
/// Apply the INVERSE of each operation in `block`, in reverse declaration order.
132+
///
133+
/// This implements JtV v2's core claim: `reverse { x += v }` IS subtraction.
134+
/// The inverse of `AddAssign` is `SubAssign`, and vice versa. Operations are
135+
/// applied in reverse order so that multi-step blocks invert correctly.
136+
///
137+
/// Expressions are evaluated against the state as it evolves through the
138+
/// inversion (same semantics as the forward pass, but in reverse).
139+
pub fn execute_inverse(&mut self, block: &ReverseBlock) -> Result<()> {
140+
for stmt in block.body.iter().rev() {
141+
self.execute_inverted_stmt(stmt)?;
142+
}
143+
Ok(())
144+
}
145+
146+
fn execute_inverted_stmt(&mut self, stmt: &ReversibleStmt) -> Result<()> {
147+
match stmt {
148+
ReversibleStmt::AddAssign(target, expr) => {
149+
// Inverse of x += v is x -= v
150+
let value = self.eval_data_expr(expr)?;
151+
let current = self.get_variable(target)?;
152+
let neg_value = value.negate()?;
153+
let result = current.add(&neg_value)?;
154+
self.variables.insert(target.clone(), result);
155+
Ok(())
156+
}
157+
ReversibleStmt::SubAssign(target, expr) => {
158+
// Inverse of x -= v is x += v
159+
let value = self.eval_data_expr(expr)?;
160+
let current = self.get_variable(target)?;
161+
let result = current.add(&value)?;
162+
self.variables.insert(target.clone(), result);
163+
Ok(())
164+
}
165+
ReversibleStmt::If(if_stmt) => {
166+
// Re-evaluate the condition on current state (Janus requirement:
167+
// the condition must hold in the same direction when reversing).
168+
let cond = self.eval_control_expr(&if_stmt.condition)?;
169+
if cond.is_truthy() {
170+
for ctrl_stmt in if_stmt.then_branch.iter().rev() {
171+
if let crate::ast::ControlStmt::ReverseBlock(inner) = ctrl_stmt {
172+
self.execute_inverse(inner)?;
173+
}
174+
// Non-reverse stmts in reverse-block if branches are
175+
// rejected by check_reversibility before we get here.
176+
}
177+
} else if let Some(else_branch) = &if_stmt.else_branch {
178+
for ctrl_stmt in else_branch.iter().rev() {
179+
if let crate::ast::ControlStmt::ReverseBlock(inner) = ctrl_stmt {
180+
self.execute_inverse(inner)?;
181+
}
182+
}
183+
}
184+
Ok(())
185+
}
186+
}
187+
}
188+
129189
fn execute_reversible_stmt(&mut self, stmt: &ReversibleStmt) -> Result<()> {
130190
match stmt {
131191
ReversibleStmt::AddAssign(target, expr) => {
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// (MPL-2.0 is automatic legal fallback until PMPL is formally recognised)
3+
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
5+
//! JtV v2 reversibility Phase 1 tests.
6+
//!
7+
//! Verifies that `reverse { x += v }` IS subtraction (x = x - v), per the
8+
//! canonical design in `docs/language/DESIGN-JTV-V2-REVERSIBILITY.md`.
9+
//!
10+
//! Subtraction is not a grammar primitive in JtV v2. It arises from
11+
//! reversing addition. `reverse { x += v }` does NOT do a forward pass —
12+
//! it applies the INVERSE of each operation in reverse declaration order.
13+
//!
14+
//! RV1 — single add: reverse { x += 5 } → x = x - 5
15+
//! RV2 — single sub: reverse { x -= 3 } → x = x + 3
16+
//! RV3 — chain: reverse { x += 5; y += 3 } → y -= 3 first, then x -= 5
17+
//! RV4 — cross-var: reverse { x += y } → x = x - y
18+
//! RV5 — CNO via execute_and_reverse: net effect is identity
19+
//! RV6 — full-program parse + run with reverse { }
20+
//! RV7 — reverse is left-inverse of forward: (forward; reverse) = identity
21+
22+
use jtv_core::{
23+
ast::{DataExpr, Number, ReverseBlock, ReversibleStmt},
24+
number::Value,
25+
parser::parse_program,
26+
reversible::ReversibleInterpreter,
27+
Interpreter,
28+
};
29+
30+
// ── RV1: reverse add is subtract ────────────────────────────────────────────
31+
32+
#[test]
33+
fn rv1_reverse_add_is_subtract() {
34+
let mut interp = Interpreter::new();
35+
let src = r#"
36+
x = 10
37+
reverse { x += 5 }
38+
"#;
39+
let prog = parse_program(src).expect("should parse");
40+
interp.run(&prog).expect("should run");
41+
42+
let x = interp.get_variables().into_iter()
43+
.find(|(k, _)| k == "x").map(|(_, v)| v);
44+
assert_eq!(x, Some(Value::Int(5)),
45+
"reverse {{ x += 5 }} with x=10 should give x=5 (subtraction)");
46+
}
47+
48+
// ── RV2: reverse sub is add ──────────────────────────────────────────────────
49+
50+
#[test]
51+
fn rv2_reverse_sub_is_add() {
52+
let mut interp = Interpreter::new();
53+
let src = r#"
54+
x = 10
55+
reverse { x -= 3 }
56+
"#;
57+
let prog = parse_program(src).expect("should parse");
58+
interp.run(&prog).expect("should run");
59+
60+
let x = interp.get_variables().into_iter()
61+
.find(|(k, _)| k == "x").map(|(_, v)| v);
62+
assert_eq!(x, Some(Value::Int(13)),
63+
"reverse {{ x -= 3 }} with x=10 should give x=13 (inverse of subtraction is addition)");
64+
}
65+
66+
// ── RV3: multi-op chain inverts in reverse order ─────────────────────────────
67+
68+
#[test]
69+
fn rv3_chain_inverts_in_reverse_declaration_order() {
70+
// reverse { x += 5 ; y += 3 }
71+
// Should apply: y -= 3 first, then x -= 5
72+
// Both are independent, so order doesn't matter here; test values confirm semantics.
73+
let mut interp = Interpreter::new();
74+
let src = r#"
75+
x = 10
76+
y = 20
77+
reverse { x += 5 y += 3 }
78+
"#;
79+
let prog = parse_program(src).expect("should parse");
80+
interp.run(&prog).expect("should run");
81+
82+
let vars: std::collections::HashMap<String, Value> =
83+
interp.get_variables().into_iter().collect();
84+
assert_eq!(vars.get("x"), Some(&Value::Int(5)),
85+
"x should be 10-5=5");
86+
assert_eq!(vars.get("y"), Some(&Value::Int(17)),
87+
"y should be 20-3=17");
88+
}
89+
90+
// ── RV4: cross-variable: reverse { x += y } → x = x - y ────────────────────
91+
92+
#[test]
93+
fn rv4_cross_variable_reverse() {
94+
let mut interp = Interpreter::new();
95+
let src = r#"
96+
x = 10
97+
y = 3
98+
reverse { x += y }
99+
"#;
100+
let prog = parse_program(src).expect("should parse");
101+
interp.run(&prog).expect("should run");
102+
103+
let x = interp.get_variables().into_iter()
104+
.find(|(k, _)| k == "x").map(|(_, v)| v);
105+
assert_eq!(x, Some(Value::Int(7)),
106+
"reverse {{ x += y }} with x=10, y=3 should give x=7");
107+
}
108+
109+
// ── RV5: CNO via execute_and_reverse ────────────────────────────────────────
110+
111+
#[test]
112+
fn rv5_cno_execute_and_reverse_is_identity() {
113+
let mut interp = ReversibleInterpreter::new();
114+
interp.set("x".to_string(), Value::Int(42));
115+
interp.set("y".to_string(), Value::Int(100));
116+
117+
let block = ReverseBlock {
118+
body: vec![
119+
ReversibleStmt::AddAssign("x".to_string(), DataExpr::Number(Number::Int(7))),
120+
ReversibleStmt::AddAssign("y".to_string(), DataExpr::Number(Number::Int(15))),
121+
],
122+
};
123+
124+
interp.execute_and_reverse(&block).expect("CNO should not fail");
125+
126+
assert_eq!(interp.get("x"), Some(&Value::Int(42)), "x must be unchanged after CNO");
127+
assert_eq!(interp.get("y"), Some(&Value::Int(100)), "y must be unchanged after CNO");
128+
}
129+
130+
// ── RV6: full-program parse + run ────────────────────────────────────────────
131+
132+
#[test]
133+
fn rv6_full_program_with_reverse_block() {
134+
// A meaningful program: accumulate a total, then reverse one step.
135+
// JtV top-level uses `x = expr` for assignment; `+=` is reverse-block only.
136+
let src = r#"
137+
total = 100
138+
bonus = 25
139+
total = total + bonus
140+
reverse { total += bonus }
141+
"#;
142+
// After: total = 100 + 25 = 125; then reverse { total += 25 } → total = 125 - 25 = 100
143+
let mut interp = Interpreter::new();
144+
let prog = parse_program(src).expect("should parse");
145+
interp.run(&prog).expect("should run");
146+
147+
let vars: std::collections::HashMap<String, Value> =
148+
interp.get_variables().into_iter().collect();
149+
assert_eq!(vars.get("total"), Some(&Value::Int(100)),
150+
"forward +25 then reverse -25 returns total to 100");
151+
assert_eq!(vars.get("bonus"), Some(&Value::Int(25)), "bonus unchanged");
152+
}
153+
154+
// ── RV7: forward then reverse is left-inverse ────────────────────────────────
155+
156+
#[test]
157+
fn rv7_forward_then_reverse_is_left_inverse() {
158+
// `execute_forward` then `execute_inverse` on same block = identity.
159+
// This is distinct from `execute_and_reverse` (CNO): here we call
160+
// forward on one interpreter, then inverse on another starting from
161+
// the FORWARD state — proving they are mutual inverses.
162+
let block = ReverseBlock {
163+
body: vec![
164+
ReversibleStmt::AddAssign("x".to_string(), DataExpr::Number(Number::Int(11))),
165+
],
166+
};
167+
168+
let mut fwd = ReversibleInterpreter::new();
169+
fwd.set("x".to_string(), Value::Int(5));
170+
fwd.execute_forward(&block).expect("forward");
171+
assert_eq!(fwd.get("x"), Some(&Value::Int(16)), "forward: x = 5 + 11 = 16");
172+
173+
// Now apply inverse starting from the forward state
174+
let mut inv = ReversibleInterpreter::with_state(fwd.get_state().clone());
175+
inv.execute_inverse(&block).expect("inverse");
176+
assert_eq!(inv.get("x"), Some(&Value::Int(5)),
177+
"inverse of forward restores original: 16 - 11 = 5");
178+
}

0 commit comments

Comments
 (0)