Skip to content

Commit 0c50296

Browse files
committed
refactor(echo): Safe-only reverse-block policy + rustfmt
Per design decision: reverse blocks accept only EchoSafe; EchoNeutral and EchoBreaking are both rejected (stricter than spec §9, which permits Neutral via residue reversal — deferred until that runtime mechanism exists). - echo.rs: admissible_in_reverse ⇔ Safe; tests + docs updated; rustfmt-clean. - typechecker.rs: EchoViolation message reflects Safe-only policy. - JtvEcho.lean: Echo.admissible is Safe-only; admissible_iff ↔ e = safe. join_admissible / blockEcho_admissible still hold (join is safe ⇔ both safe). - spec §9, ALIGNMENT, PROOF matrix: record the Safe-only policy + the four Echo design decisions (effect-only; Safe-only; infer+@echo; bridge deferred). https://claude.ai/code/session_01EJLZKDtcF1RGdKx6TcUTQG
1 parent 57d3511 commit 0c50296

6 files changed

Lines changed: 63 additions & 37 deletions

File tree

ALIGNMENT-AFFINESCRIPT.adoc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,28 @@ truthing discipline, because a mechanized proof cannot lie.
115115
* [ ] Wire `JtvEcho` into the `proof-regression.yml` gate (already covered by
116116
the `lakefile.lean` default target).
117117

118+
==== Design decisions (2026-06-02)
119+
120+
. *Echo is effect-only* — a static dimension alongside `Purity`, not a value
121+
type (spec §12). A first-class `Echo<T>` value type is reserved for a later
122+
phase.
123+
. *Safe-only reversal* — reverse blocks accept only `EchoSafe`; `Neutral` and
124+
`Breaking` are rejected (see §9 implementation note).
125+
. *Echo source = infer + `@echo(...)` override* — inferred from operation shape,
126+
with an optional annotation mirroring `@pure`/`@total`. Lossy numeric widening
127+
(e.g. `Float→Int`) is to carry `Neutral` echo.
128+
. *Agda↔Lean proof bridge deferred* — `JtvEcho.lean` re-mechanizes the model
129+
independently for now; `echo-types` remains the Agda source of truth.
130+
118131
=== Phase 2 — Echo Types in the checker (commenced here)
119132

120133
* [x] Implement the Echo effect lattice in `crates/jtv-core/src/echo.rs`
121134
(`Safe ⊑ Neutral ⊑ Breaking`, join, admissibility) — the executable image of
122135
`JtvEcho.lean`.
123-
* [x] Enforce spec §9 in the type checker: a `reverse` block is rejected when
124-
its aggregate echo is `Breaking` (`JtvError::EchoViolation`).
136+
* [x] Enforce reversibility in the type checker (Safe-only policy): a `reverse`
137+
block is rejected unless every statement is `EchoSafe` — `Neutral` and
138+
`Breaking` both raise `JtvError::EchoViolation`. (Spec §9 permits `Neutral` via
139+
residue reversal; deferred until that runtime mechanism exists.)
125140
* [ ] Propagate echo through Data inference as a second effect dimension
126141
(alongside `Purity`); surface `@echo(...)` annotations.
127142
* [ ] Bridge value-level fibres (`echo-types`) to the effect classes for

crates/jtv-core/src/echo.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,17 @@ impl Echo {
5252
}
5353

5454
/// Whether this echo may appear inside a reverse block.
55-
/// Spec v2 §9: reverse blocks forbid `Breaking` (information-destroying)
56-
/// statements. Corresponds to `Echo.admissible` in `JtvEcho.lean`.
55+
///
56+
/// Policy: **Safe-only.** A reverse block must be fully reversible, so only
57+
/// `EchoSafe` (bijective `+`/`-`) statements are admissible. `EchoNeutral`
58+
/// is rejected too: although spec v2 §9 permits it *in principle*
59+
/// (reversal via a retained residue, Bennett-style), that runtime mechanism
60+
/// is not implemented, so the checker conservatively requires `Safe`.
61+
/// `EchoBreaking` is of course always rejected.
62+
///
63+
/// Corresponds to `Echo.admissible` in `JtvEcho.lean`.
5764
pub fn admissible_in_reverse(self) -> bool {
58-
self != Echo::Breaking
65+
self == Echo::Safe
5966
}
6067
}
6168

@@ -159,42 +166,34 @@ mod tests {
159166
assert!(Neutral.leq(Breaking));
160167
assert!(Safe.leq(Breaking));
161168
assert!(!Breaking.leq(Safe));
169+
// Safe-only reversal policy: only Safe is admissible in a reverse block.
162170
assert!(Safe.admissible_in_reverse());
163-
assert!(Neutral.admissible_in_reverse());
171+
assert!(!Neutral.admissible_in_reverse());
164172
assert!(!Breaking.admissible_in_reverse());
165173
}
166174

167175
#[test]
168176
fn add_assign_independent_is_safe() {
169177
// x += y (y independent of x) -> Safe
170-
let stmt = ReversibleStmt::AddAssign(
171-
"x".to_string(),
172-
DataExpr::Identifier("y".to_string()),
173-
);
178+
let stmt =
179+
ReversibleStmt::AddAssign("x".to_string(), DataExpr::Identifier("y".to_string()));
174180
assert_eq!(classify_reversible_stmt(&stmt), Echo::Safe);
175181
}
176182

177183
#[test]
178184
fn self_reference_is_breaking() {
179185
// x += x destroys the original x -> Breaking
180-
let stmt = ReversibleStmt::AddAssign(
181-
"x".to_string(),
182-
DataExpr::Identifier("x".to_string()),
183-
);
186+
let stmt =
187+
ReversibleStmt::AddAssign("x".to_string(), DataExpr::Identifier("x".to_string()));
184188
assert_eq!(classify_reversible_stmt(&stmt), Echo::Breaking);
185189
}
186190

187191
#[test]
188192
fn block_breaking_iff_any_breaking() {
189193
// [Safe, Safe] -> Safe ; one Breaking poisons the block.
190-
let safe = ReversibleStmt::AddAssign(
191-
"x".to_string(),
192-
DataExpr::Number(Number::Int(5)),
193-
);
194-
let breaking = ReversibleStmt::AddAssign(
195-
"y".to_string(),
196-
DataExpr::Identifier("y".to_string()),
197-
);
194+
let safe = ReversibleStmt::AddAssign("x".to_string(), DataExpr::Number(Number::Int(5)));
195+
let breaking =
196+
ReversibleStmt::AddAssign("y".to_string(), DataExpr::Identifier("y".to_string()));
198197
assert_eq!(classify_stmts(&[safe.clone()]), Echo::Safe);
199198
assert_eq!(classify_stmts(&[safe.clone(), breaking]), Echo::Breaking);
200199
}

crates/jtv-core/src/typechecker.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,11 +409,12 @@ impl TypeChecker {
409409
let aggregate = echo::classify_stmts(body);
410410
if !aggregate.admissible_in_reverse() {
411411
return Err(JtvError::EchoViolation(format!(
412-
"reverse block has echo {aggregate}: it destroys information \
413-
and cannot be inverted. Reverse blocks may only contain \
414-
{} or {} statements (no total erasure).",
412+
"reverse block has echo {aggregate}: it is not fully reversible. \
413+
Reverse blocks may only contain {} statements (bijective +/-); \
414+
lossy ({} / {}) operations are not invertible here.",
415415
Echo::Safe,
416-
Echo::Neutral
416+
Echo::Neutral,
417+
Echo::Breaking
417418
)));
418419
}
419420
Ok(())

jtv_proofs/JtvEcho.lean

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
Lattice order: `safe ⊑ neutral ⊑ breaking` (join moves rightward, losing
1919
guarantees — mirroring the effect lattice `Total ⊑ Pure ⊑ Impure`).
2020
21-
The headline result for the type checker is `blockEcho_admissible`: a reverse
22-
block is admissible iff *no* constituent statement is `breaking`. This is the
23-
formal contract enforced by `crates/jtv-core/src/echo.rs`.
21+
The headline result for the type checker is `blockEcho_admissible`: under the
22+
**Safe-only** reversal policy, a reverse block is admissible iff *every*
23+
constituent statement is `safe` (fully reversible). `neutral` (lossy but
24+
residue-retaining) and `breaking` are both rejected. This is the formal
25+
contract enforced by `crates/jtv-core/src/echo.rs`.
2426
2527
Proofs are deliberately Mathlib-free (Lean core only) and discharge every
2628
goal by finite case analysis, matching the style of the other JtV proofs.
@@ -103,13 +105,16 @@ theorem le_breaking (a : Echo) : a ≤ Echo.breaking := by
103105
-- SECTION 2: REVERSE-BLOCK ADMISSIBILITY (the type-checker contract)
104106
-- ============================================================================
105107

106-
/-- Boolean admissibility: an Echo may appear in a reverse block iff it does
107-
not totally erase information (spec v2 §9 forbids `EchoBreaking`). -/
108+
/-- Boolean admissibility: an Echo may appear in a reverse block iff it is
109+
`safe`. This is the **Safe-only** policy: a reverse block must be fully
110+
reversible, so lossy `neutral` operations (whose Bennett-style residue
111+
reversal is not yet implemented) are rejected alongside `breaking`. Spec v2
112+
§9 permits `neutral` in principle; the checker is conservatively stricter. -/
108113
def Echo.admissible : Echo → Bool
109-
| .breaking => false
110-
| _ => true
114+
| .safe => true
115+
| _ => false
111116

112-
theorem admissible_iff (e : Echo) : e.admissible = true ↔ e Echo.breaking := by
117+
theorem admissible_iff (e : Echo) : e.admissible = true ↔ e = Echo.safe := by
113118
cases e <;> simp [Echo.admissible]
114119

115120
/-- **Key compositional law.** The echo of a composite operation `a ⊔ b` is
@@ -142,8 +147,8 @@ def allAdmissible : List Echo → Bool
142147
| e :: es => e.admissible && allAdmissible es
143148

144149
/-- **Reverse-block soundness.** A block's aggregate echo is admissible iff
145-
every statement in it is admissible — i.e. a reverse block is well-typed
146-
exactly when it contains no `breaking` (information-destroying) statement.
150+
every statement in it is admissible — i.e. (under the Safe-only policy) a
151+
reverse block is well-typed exactly when every statement is `safe`.
147152
148153
This is the property `echo::classify_stmts` / the reverse-block gate in
149154
`crates/jtv-core/src/echo.rs` must implement. -/

spec/jtv_v2_type_system_corrected.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ Allowed only if:
127127
- no EchoBreaking statements
128128
- no loops in V2
129129
130+
Implementation note (2026-06-02): the checker enforces a *stricter, Safe-only*
131+
policy — every statement must be `EchoSafe`. `EchoNeutral` is permitted by this
132+
spec in principle (reversal via a retained residue, Bennett-style), but that
133+
runtime mechanism is unimplemented, so the checker conservatively rejects it.
134+
See `crates/jtv-core/src/echo.rs` and `jtv_proofs/JtvEcho.lean`.
135+
130136
== 10. Soundness Theorem
131137
132138
Theorem (Safe Fragment Termination):

verification/PROOF-CAPABILITY-MATRIX.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ induction. Corrected as part of the Phase-1 doc-truthing pass.
7070
| `string_not_executable` | JtvSecurity | `verified` | User strings are never code (injection impossibility).
7171
| `strong_normalization` | JtvExtended | `verified` | Data fragment strongly normalizing.
7272
| `confluence` | JtvExtended | `verified` | Data reduction confluent.
73-
| `blockEcho_admissible` | JtvEcho | `verified` | Reverse block admissible ⇔ no `Breaking` statement (the checker contract).
73+
| `blockEcho_admissible` | JtvEcho | `verified` | Reverse block admissible ⇔ every statement `Safe` (Safe-only policy; the checker contract).
7474
| `injective_fibre_subsingleton` | JtvEcho | `verified` | `EchoSafe` ⇔ injective ⇔ recoverable.
7575
| `residue_lossy` | JtvEcho | `verified` | Structured loss is genuine and one-way.
7676
| `parser_correctness` | — | `absent` | Parser produces a valid AST. (PROOF-1, High.)

0 commit comments

Comments
 (0)