Skip to content

Commit a0a2397

Browse files
feat(types): echo-types integration — Echo type former (slice 1) (#86)
Adds EchoMode { Linear, Affine }, Ty::Echo { mode, domain, codomain }, and affine-weakening assignability implementing EchoLinear.weaken / no-section-weaken. 5 unit tests mirror the Agda theorems. Claim boundary held to the narrowed loss-graded reindexing modality over a thin poset (retraction R-2026-05-18). Design note in docs/design/echo-types-integration.md.
2 parents 6acb330 + ab2b7fb commit a0a2397

2 files changed

Lines changed: 283 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+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
3+
4+
# Design note: integrating `echo-types` into the my-lang type checker
5+
6+
**Status:** accepted (slice 1) · **Date:** 2026-06-02 · **PR:** #86
7+
**Upstream:** [`hyperpolymath/echo-types`](https://github.com/hyperpolymath/echo-types)
8+
(Agda library) · executable companion
9+
[`hyperpolymath/EchoTypes.jl`](https://github.com/hyperpolymath/EchoTypes.jl)
10+
11+
This note records the design decisions for bringing *echo types* — a
12+
formal account of **"loss that is not total erasure"** — into the my-lang
13+
type system. It documents what slice 1 (PR #86) deliberately is and is
14+
not, so later slices stay on the agreed line.
15+
16+
## What an echo type is (upstream)
17+
18+
In `echo-types`, given `f : A → B`, the **echo** at `y : B` is the fiber
19+
20+
```
21+
Echo f y = Σ (x : A), (f x ≡ y)
22+
```
23+
24+
— a *proof-relevant* witness of which inputs collapsed to `y`. It captures
25+
the third case between reversible (no loss) and irreversible (total
26+
erasure): **irreversible-but-with-a-retained, typed constraint on what was
27+
lost**.
28+
29+
## 1. Index shape — non-dependent approximation
30+
31+
my-lang is **not** dependently typed, so we do not index echoes by a
32+
specific function `f` and output `y`. Forcing value-indexing now would drag
33+
the checker into refinement/dependent territory before the language is
34+
ready.
35+
36+
**Canonical form:**
37+
38+
```
39+
Echo<A => B> // internal: Ty::Echo { mode, domain: A, codomain: B }
40+
```
41+
42+
read as *"a retained residue/witness of some admissible collapse from `A`
43+
to `B`"***not** the complete fiber of a specific `f` at `y`.
44+
45+
- **Keep** the map-shaped form `Echo<A => B>` as the semantic core; the
46+
source/target asymmetry is essential.
47+
- **Possible sugar later (only if needed):** `Echo<T> := Echo<T => T>` (or
48+
`Echo<Unknown => T>`). `Echo<T>` is *not* the primary form — it is too
49+
modal and discards the asymmetry.
50+
- **Avoid for now:** the value-indexed/dependent `Echo<f, y>` form.
51+
52+
The Agda `Echo f y` remains the *semantic ideal*; `Echo<A => B>` is its
53+
deliberate erased approximation.
54+
55+
## 2. Relationship to the quantity (affine/QTT) system
56+
57+
Echo is **born from** the quantity system but is **not identical** to it:
58+
59+
```
60+
Quantity controls USE.
61+
Echo records the structured LOSS caused by weakening / use-erasure.
62+
```
63+
64+
Design invariant:
65+
66+
> A linear value weakened to affine may **produce** an `Echo` residue. The
67+
> weakening is one-way. The residue is proof-relevant evidence that
68+
> something was lost but **not annihilated**.
69+
70+
The first (and, for now, only) bridge is therefore **linear → affine**.
71+
Echo is **not** made fully orthogonal to quantities yet; orthogonal
72+
modalities (epistemic/security/provenance) can come later.
73+
74+
## 3. Mode and subtyping (slice 1, implemented)
75+
76+
`EchoMode { Linear, Affine }` mirrors `EchoLinear.agda`'s two-point thin
77+
poset `linear ⊑ affine`. Assignability (`Ty::is_assignable_from`) encodes:
78+
79+
```
80+
Echo Linear <: Echo Affine (EchoLinear.weaken)
81+
Echo Affine </: Echo Linear (EchoLinear.no-section-weaken)
82+
domain / codomain invariant
83+
```
84+
85+
The five unit tests in `crates/my-lang/src/types.rs` mirror the Agda
86+
theorems directly: mode ordering (`_≤m_`), `weaken`, `no-section`, and
87+
domain/codomain invariance.
88+
89+
## 4. Bridges — scope
90+
91+
| Bridge | Status |
92+
|--------|--------|
93+
| linear / affine | **in scope now** (slice 1) |
94+
| provenance / audit | next candidate (design-noted, not implemented) |
95+
| epistemic | later |
96+
| security | later |
97+
| graded / tropical | research layer |
98+
| choreographic | separate bridge |
99+
| ordinal | definitely later |
100+
101+
Rationale: provenance/audit/security are tempting because they fit
102+
my-lang's AI/audit features, but introducing them early would contaminate
103+
the core. Slice 1 answers exactly one question — *what proof-relevant
104+
residue remains when linear information is weakened?* — and the audit
105+
bridge later reuses that answer.
106+
107+
## 5. Surface syntax & runtime — staging
108+
109+
First dialect: **solo** (the affine one). Canonical syntax `Echo<A => B>`.
110+
The lossy-arrow `~>` is **reserved** for possible later sugar (lossy
111+
function / operation), not introduced now to avoid overloading. Witnesses
112+
are **erased** initially.
113+
114+
```
115+
Stage 1 (this PR): checker-only Echo type former ← done
116+
Stage 2: parser syntax for Echo<A => B> (solo)
117+
Stage 3: proof-layer residue object (EchoResidue, Coq/Idris)
118+
Stage 4: optional runtime witness — only if audit/provenance needs
119+
observable evidence; modelled as a single residue record,
120+
not a whole fiber
121+
```
122+
123+
## 6. Claim boundary
124+
125+
Hold the narrowed line from `echo-types` retraction **R-2026-05-18**:
126+
127+
> Echo is a **loss-graded reindexing modality over a thin poset**.
128+
129+
Permitted, modest, strong claim:
130+
131+
> Echo models irreversible weakening as a one-way, proof-relevant residue
132+
> transformation. Linear Echo can weaken to Affine Echo. Affine Echo cannot
133+
> reconstruct Linear Echo.
134+
135+
**Do not** use (unless a future proof actually justifies it): "graded
136+
comonad", "universal property", "adjunction", "final/initial semantics",
137+
"free construction".
138+
139+
## 7. EchoTypes.jl as differential oracle
140+
141+
Phase-2, not a blocker for slice 1. The current Rust tests already mirror
142+
the `EchoLinear.agda` weakening/no-section theorems at the type-algebra
143+
level. Once parser syntax or proof-layer residue terms exist, add
144+
finite-domain differential tests against EchoTypes.jl's `LEcho` / `EchoMode`
145+
— for semantic examples, **not** core checker soundness.
146+
147+
## Slice 1 scope (PR #86) — exactly
148+
149+
- `EchoMode { Linear, Affine }`
150+
- `Ty::Echo { mode, domain, codomain }`
151+
- assignability: `Linear → Affine` only; domain/codomain invariant
152+
- `Display`
153+
- five Agda-mirroring tests
154+
155+
No parser, codegen, runtime, or proof-layer changes in this PR.

0 commit comments

Comments
 (0)