Skip to content

Commit b3c4e74

Browse files
committed
feat(types): add Echo type former with affine weaken semantics
First slice of echo-types integration (hyperpolymath/echo-types) into the my-lang type checker. Adds the Echo type former to the checker's internal type algebra (crates/my-lang/src/types.rs): - `EchoMode { Linear, Affine }` mirroring EchoLinear.agda's two-point thin poset `linear ⊑ affine`. - `Ty::Echo { mode, domain, codomain }` — the proof-relevant residue of a lossy map `Echo f y = Σ(x:domain)(f x ≡ codomain-point)`, i.e. "loss that is not total erasure". - Assignability encodes EchoLinear `weaken` + `no-section-weaken`: a Linear echo may be supplied where an Affine one is expected, never the reverse; domain/codomain invariant. - Display + 5 unit tests mirroring the Agda theorems (mode order, weaken, no-section, invariance). `my-lang` and its core consumer crates build green; all existing tests pass (echo matches slot into existing wildcard arms — no blast radius). Scope: internal type algebra + checker only. Surface syntax, codegen, the mechanised EchoResidue proof layer, and bridges beyond linear/affine are follow-ups pending design decisions (see PR discussion). https://claude.ai/code/session_01VHjceAPA5ZARUe7ESWNQPF
1 parent 4cd7bb1 commit b3c4e74

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

crates/my-lang/src/types.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,46 @@
44
55
use std::fmt;
66

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+
747
/// Internal type representation used during type checking
848
#[derive(Debug, Clone, PartialEq)]
949
pub enum Ty {
@@ -46,6 +86,20 @@ pub enum Ty {
4686
/// Effect type
4787
Effect(Box<Ty>),
4888

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+
49103
/// Type variable (for inference)
50104
Var(usize),
51105

@@ -103,6 +157,17 @@ impl Ty {
103157
&& p1.iter().zip(p2.iter()).all(|(x, y)| y.is_assignable_from(x)) // contravariant
104158
&& r1.is_assignable_from(r2) // covariant
105159
}
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+
}
106171
_ => false,
107172
}
108173
}
@@ -143,6 +208,9 @@ impl fmt::Display for Ty {
143208
}
144209
Ty::AI(inner) => write!(f, "AI<{}>", inner),
145210
Ty::Effect(inner) => write!(f, "Effect<{}>", inner),
211+
Ty::Echo { mode, domain, codomain } => {
212+
write!(f, "{} Echo<{} => {}>", mode, domain, codomain)
213+
}
146214
Ty::Var(id) => write!(f, "?{}", id),
147215
Ty::Error => write!(f, "<error>"),
148216
Ty::Unknown => write!(f, "<unknown>"),
@@ -182,3 +250,63 @@ pub fn ast_type_to_ty(ty: &crate::ast::Type) -> Ty {
182250
Type::Constrained { base, .. } => ast_type_to_ty(base),
183251
}
184252
}
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

Comments
 (0)