|
4 | 4 |
|
5 | 5 | use std::fmt; |
6 | 6 |
|
| 7 | +/// Linearity mode of an echo residue. |
| 8 | +/// |
| 9 | +/// Mirrors `Mode` in the `echo-types` Agda library (`EchoLinear.agda`): |
| 10 | +/// the thin two-point poset `linear ⊑ affine`. `Linear` keeps the full, |
| 11 | +/// proof-relevant fiber witness; `Affine` keeps only the collapsed |
| 12 | +/// residue (all affine echoes over a point are equal — `affine-all-equal`). |
| 13 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 14 | +pub enum EchoMode { |
| 15 | + /// Full fiber retained; distinctions preserved. |
| 16 | + Linear, |
| 17 | + /// Collapsed residue; distinctions weakened away. |
| 18 | + Affine, |
| 19 | +} |
| 20 | + |
| 21 | +impl EchoMode { |
| 22 | + /// The mode ordering `_≤m_` from `EchoLinear.agda` |
| 23 | + /// (`linear ≤m linear`, `linear ≤m affine`, `affine ≤m affine`). |
| 24 | + /// |
| 25 | + /// `self.weakens_to(other)` is true when an echo at mode `self` may be |
| 26 | + /// *weakened* to mode `other`. Crucially `Affine` does **not** weaken to |
| 27 | + /// `Linear`: weakening is lossy and has no section (`no-section-weaken`). |
| 28 | + pub fn weakens_to(self, other: EchoMode) -> bool { |
| 29 | + matches!( |
| 30 | + (self, other), |
| 31 | + (EchoMode::Linear, EchoMode::Linear) |
| 32 | + | (EchoMode::Linear, EchoMode::Affine) |
| 33 | + | (EchoMode::Affine, EchoMode::Affine) |
| 34 | + ) |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +impl fmt::Display for EchoMode { |
| 39 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 40 | + match self { |
| 41 | + EchoMode::Linear => write!(f, "linear"), |
| 42 | + EchoMode::Affine => write!(f, "affine"), |
| 43 | + } |
| 44 | + } |
| 45 | +} |
| 46 | + |
7 | 47 | /// Internal type representation used during type checking |
8 | 48 | #[derive(Debug, Clone, PartialEq)] |
9 | 49 | pub enum Ty { |
@@ -46,6 +86,20 @@ pub enum Ty { |
46 | 86 | /// Effect type |
47 | 87 | Effect(Box<Ty>), |
48 | 88 |
|
| 89 | + /// Echo type — the proof-relevant residue of a lossy map, after the |
| 90 | + /// `echo-types` library: `Echo f y = Σ (x : domain) (f x ≡ y)`. We track |
| 91 | + /// the collapsing map's `domain` and `codomain` plus the linearity |
| 92 | + /// `mode`. It models "loss that is not total erasure": a typed witness of |
| 93 | + /// what a `domain` value retained after collapsing into `codomain`. |
| 94 | + /// `mode` records whether the full fiber (`Linear`) or only the collapsed |
| 95 | + /// residue (`Affine`) is observable. See the affine bridge in |
| 96 | + /// `proofs/verification/{coq,idris}/solo-core/EchoResidue.*`. |
| 97 | + Echo { |
| 98 | + mode: EchoMode, |
| 99 | + domain: Box<Ty>, |
| 100 | + codomain: Box<Ty>, |
| 101 | + }, |
| 102 | + |
49 | 103 | /// Type variable (for inference) |
50 | 104 | Var(usize), |
51 | 105 |
|
@@ -103,6 +157,17 @@ impl Ty { |
103 | 157 | && p1.iter().zip(p2.iter()).all(|(x, y)| y.is_assignable_from(x)) // contravariant |
104 | 158 | && r1.is_assignable_from(r2) // covariant |
105 | 159 | } |
| 160 | + ( |
| 161 | + Ty::Echo { mode: m_t, domain: d_t, codomain: c_t }, |
| 162 | + Ty::Echo { mode: m_s, domain: d_s, codomain: c_s }, |
| 163 | + ) => { |
| 164 | + // Echo weakening (EchoLinear `weaken` / `_≤m_`): a more- |
| 165 | + // informative source echo may be supplied where a less- |
| 166 | + // informative target is expected — `Linear` weakens to |
| 167 | + // `Affine`, never the reverse (the weakening has no section, |
| 168 | + // `no-section-weaken`). Domain and codomain are invariant. |
| 169 | + m_s.weakens_to(*m_t) && d_t == d_s && c_t == c_s |
| 170 | + } |
106 | 171 | _ => false, |
107 | 172 | } |
108 | 173 | } |
@@ -143,6 +208,9 @@ impl fmt::Display for Ty { |
143 | 208 | } |
144 | 209 | Ty::AI(inner) => write!(f, "AI<{}>", inner), |
145 | 210 | Ty::Effect(inner) => write!(f, "Effect<{}>", inner), |
| 211 | + Ty::Echo { mode, domain, codomain } => { |
| 212 | + write!(f, "{} Echo<{} => {}>", mode, domain, codomain) |
| 213 | + } |
146 | 214 | Ty::Var(id) => write!(f, "?{}", id), |
147 | 215 | Ty::Error => write!(f, "<error>"), |
148 | 216 | Ty::Unknown => write!(f, "<unknown>"), |
@@ -182,3 +250,63 @@ pub fn ast_type_to_ty(ty: &crate::ast::Type) -> Ty { |
182 | 250 | Type::Constrained { base, .. } => ast_type_to_ty(base), |
183 | 251 | } |
184 | 252 | } |
| 253 | + |
| 254 | +#[cfg(test)] |
| 255 | +mod echo_tests { |
| 256 | + use super::*; |
| 257 | + |
| 258 | + fn echo(mode: EchoMode) -> Ty { |
| 259 | + Ty::Echo { |
| 260 | + mode, |
| 261 | + domain: Box::new(Ty::Bool), |
| 262 | + codomain: Box::new(Ty::Unit), |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + // EchoLinear `_≤m_`: linear ⊑ linear, linear ⊑ affine, affine ⊑ affine, |
| 267 | + // and crucially affine ⋢ linear. |
| 268 | + #[test] |
| 269 | + fn mode_ordering_is_thin_poset() { |
| 270 | + assert!(EchoMode::Linear.weakens_to(EchoMode::Linear)); |
| 271 | + assert!(EchoMode::Linear.weakens_to(EchoMode::Affine)); |
| 272 | + assert!(EchoMode::Affine.weakens_to(EchoMode::Affine)); |
| 273 | + assert!(!EchoMode::Affine.weakens_to(EchoMode::Linear)); |
| 274 | + } |
| 275 | + |
| 276 | + // EchoLinear `weaken`: a linear echo may be supplied where an affine |
| 277 | + // echo is expected (target.is_assignable_from(source)). |
| 278 | + #[test] |
| 279 | + fn linear_weakens_to_affine() { |
| 280 | + let affine = echo(EchoMode::Affine); |
| 281 | + let linear = echo(EchoMode::Linear); |
| 282 | + assert!(affine.is_assignable_from(&linear)); |
| 283 | + } |
| 284 | + |
| 285 | + // EchoLinear `no-section-weaken`: weakening is irreversible — an affine |
| 286 | + // echo is NOT acceptable where a linear echo is required. |
| 287 | + #[test] |
| 288 | + fn affine_has_no_section_back_to_linear() { |
| 289 | + let linear = echo(EchoMode::Linear); |
| 290 | + let affine = echo(EchoMode::Affine); |
| 291 | + assert!(!linear.is_assignable_from(&affine)); |
| 292 | + } |
| 293 | + |
| 294 | + // Same mode is reflexively assignable; mismatched domain/codomain is not. |
| 295 | + #[test] |
| 296 | + fn echo_invariant_in_domain_and_codomain() { |
| 297 | + let a = echo(EchoMode::Linear); |
| 298 | + assert!(a.is_assignable_from(&a)); |
| 299 | + let other_codomain = Ty::Echo { |
| 300 | + mode: EchoMode::Linear, |
| 301 | + domain: Box::new(Ty::Bool), |
| 302 | + codomain: Box::new(Ty::Int), |
| 303 | + }; |
| 304 | + assert!(!a.is_assignable_from(&other_codomain)); |
| 305 | + } |
| 306 | + |
| 307 | + #[test] |
| 308 | + fn echo_display_renders_mode_and_arrow() { |
| 309 | + assert_eq!(echo(EchoMode::Linear).to_string(), "linear Echo<Bool => ()>"); |
| 310 | + assert_eq!(echo(EchoMode::Affine).to_string(), "affine Echo<Bool => ()>"); |
| 311 | + } |
| 312 | +} |
0 commit comments