|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | + |
| 4 | +//! # `surface` — named-variable elaboration onto the QTT core |
| 5 | +//! |
| 6 | +//! Coupling **step 2**: the bridge from a *named* surface to the de Bruijn |
| 7 | +//! [`crate::Tm`] that the verified [`crate::check`] consumes, so the running |
| 8 | +//! compiler can enforce the QTT resource axis on real programs (today its |
| 9 | +//! `checker.rs` has none — `proofs/STATUS.md`). |
| 10 | +//! |
| 11 | +//! [`SExpr`]/[`STy`] are the **resource-relevant fragment** of the my-lang |
| 12 | +//! surface `ast::Expr`/`ast::Type` (variables, λ with a quantity, application, |
| 13 | +//! the two products, sums, `let`, the echo residue) — the schema the my-lang |
| 14 | +//! frontend lowers onto (see the `ast::Expr → SExpr` lowering, the thin |
| 15 | +//! mechanical follow-on). The non-resource surface (AI models, prompts, |
| 16 | +//! records, …) carries no usage content and is out of this fragment. |
| 17 | +//! |
| 18 | +//! Elaboration is a scope-resolution pass: names become de Bruijn indices |
| 19 | +//! against a [`Scope`] (innermost binder last, matching [`crate::Tctx`]). Types |
| 20 | +//! have no binders, so [`elaborate_ty`] is purely structural. After elaboration |
| 21 | +//! [`check_surface`] runs the verified usage-walk — so a linear binder dropped |
| 22 | +//! at the *surface* is rejected by the *machine-checked* algorithm. |
| 23 | +//! |
| 24 | +//! Quantities are explicit on [`SExpr`] binders. Because the current my-lang |
| 25 | +//! `Param` has no quantity syntax, the `ast` lowering will default binders |
| 26 | +//! (documented at the lowering site); this engine already threads whatever |
| 27 | +//! quantity the surface supplies, so adding the syntax later needs no change |
| 28 | +//! here. |
| 29 | +
|
| 30 | +use crate::{check, Mode, Q, Ty, Tm, Uvec}; |
| 31 | + |
| 32 | +/// Resource-relevant surface types (named-free; mirror of the QTT core types). |
| 33 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 34 | +pub enum STy { |
| 35 | + Unit, |
| 36 | + With(Box<STy>, Box<STy>), |
| 37 | + Tensor(Box<STy>, Box<STy>), |
| 38 | + Sum(Box<STy>, Box<STy>), |
| 39 | + /// `(q x : a) -> b` |
| 40 | + Arr(Q, Box<STy>, Box<STy>), |
| 41 | + Echo(Mode, Box<STy>, Box<STy>), |
| 42 | +} |
| 43 | + |
| 44 | +/// Resource-relevant surface terms with **named** binders. |
| 45 | +#[derive(Debug, Clone, PartialEq)] |
| 46 | +pub enum SExpr { |
| 47 | + Var(String), |
| 48 | + Unit, |
| 49 | + /// `|q x : ty| body` |
| 50 | + Lam { |
| 51 | + q: Q, |
| 52 | + param: String, |
| 53 | + ty: STy, |
| 54 | + body: Box<SExpr>, |
| 55 | + }, |
| 56 | + App(Box<SExpr>, Box<SExpr>), |
| 57 | + /// additive pair `<e1, e2>` (shared usage) |
| 58 | + With(Box<SExpr>, Box<SExpr>), |
| 59 | + Fst(Box<SExpr>), |
| 60 | + Snd(Box<SExpr>), |
| 61 | + /// multiplicative pair `(e1, e2)` (split usage) |
| 62 | + Tensor(Box<SExpr>, Box<SExpr>), |
| 63 | + /// `let (x, y) = pair in body` |
| 64 | + LetPair { |
| 65 | + x: String, |
| 66 | + y: String, |
| 67 | + pair: Box<SExpr>, |
| 68 | + body: Box<SExpr>, |
| 69 | + }, |
| 70 | + /// `inl[other] e` — `other` is the RIGHT summand annotation |
| 71 | + Inl(STy, Box<SExpr>), |
| 72 | + /// `inr[other] e` — `other` is the LEFT summand annotation |
| 73 | + Inr(STy, Box<SExpr>), |
| 74 | + /// `case scrut of inl xl => arm_l | inr xr => arm_r` |
| 75 | + Case { |
| 76 | + scrut: Box<SExpr>, |
| 77 | + xl: String, |
| 78 | + arm_l: Box<SExpr>, |
| 79 | + xr: String, |
| 80 | + arm_r: Box<SExpr>, |
| 81 | + }, |
| 82 | + /// `let (q name) = value in body` |
| 83 | + Let { |
| 84 | + q: Q, |
| 85 | + name: String, |
| 86 | + value: Box<SExpr>, |
| 87 | + body: Box<SExpr>, |
| 88 | + }, |
| 89 | + /// echo residue `[mode] echo<a => b>(witness)` |
| 90 | + Echo { |
| 91 | + mode: Mode, |
| 92 | + a: STy, |
| 93 | + b: STy, |
| 94 | + witness: Box<SExpr>, |
| 95 | + }, |
| 96 | + Weaken(Box<SExpr>), |
| 97 | +} |
| 98 | + |
| 99 | +/// A lexical scope: binder names, **innermost last** (so the de Bruijn index of |
| 100 | +/// a name is its distance from the end). Aligns with [`crate::Tctx`]. |
| 101 | +pub type Scope = Vec<String>; |
| 102 | + |
| 103 | +/// Elaboration failures (the surface fragment is otherwise total). |
| 104 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 105 | +pub enum ElabError { |
| 106 | + /// A name with no binder in scope. |
| 107 | + Unbound(String), |
| 108 | +} |
| 109 | + |
| 110 | +/// Translate a surface type to the core (purely structural — no binders). |
| 111 | +pub fn elaborate_ty(t: &STy) -> Ty { |
| 112 | + match t { |
| 113 | + STy::Unit => Ty::Unit, |
| 114 | + STy::With(a, b) => Ty::With(Box::new(elaborate_ty(a)), Box::new(elaborate_ty(b))), |
| 115 | + STy::Tensor(a, b) => Ty::Tensor(Box::new(elaborate_ty(a)), Box::new(elaborate_ty(b))), |
| 116 | + STy::Sum(a, b) => Ty::Sum(Box::new(elaborate_ty(a)), Box::new(elaborate_ty(b))), |
| 117 | + STy::Arr(q, a, b) => Ty::Arr(*q, Box::new(elaborate_ty(a)), Box::new(elaborate_ty(b))), |
| 118 | + STy::Echo(m, a, b) => Ty::Echo(*m, Box::new(elaborate_ty(a)), Box::new(elaborate_ty(b))), |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +fn lookup(scope: &Scope, name: &str) -> Option<usize> { |
| 123 | + // de Bruijn index = distance from the innermost (last) binder. |
| 124 | + scope.iter().rev().position(|n| n == name) |
| 125 | +} |
| 126 | + |
| 127 | +/// Resolve names to de Bruijn indices, producing a core [`Tm`]. `scope` holds |
| 128 | +/// the enclosing binders (innermost last); it is restored on return. |
| 129 | +pub fn elaborate(e: &SExpr, scope: &mut Scope) -> Result<Tm, ElabError> { |
| 130 | + Ok(match e { |
| 131 | + SExpr::Var(name) => { |
| 132 | + Tm::Var(lookup(scope, name).ok_or_else(|| ElabError::Unbound(name.clone()))?) |
| 133 | + } |
| 134 | + SExpr::Unit => Tm::UnitT, |
| 135 | + SExpr::Lam { q, param, ty, body } => { |
| 136 | + scope.push(param.clone()); |
| 137 | + let body = elaborate(body, scope); |
| 138 | + scope.pop(); |
| 139 | + Tm::Lam(*q, elaborate_ty(ty), Box::new(body?)) |
| 140 | + } |
| 141 | + SExpr::App(f, a) => Tm::App( |
| 142 | + Box::new(elaborate(f, scope)?), |
| 143 | + Box::new(elaborate(a, scope)?), |
| 144 | + ), |
| 145 | + SExpr::With(x, y) => Tm::With( |
| 146 | + Box::new(elaborate(x, scope)?), |
| 147 | + Box::new(elaborate(y, scope)?), |
| 148 | + ), |
| 149 | + SExpr::Fst(p) => Tm::Fst(Box::new(elaborate(p, scope)?)), |
| 150 | + SExpr::Snd(p) => Tm::Snd(Box::new(elaborate(p, scope)?)), |
| 151 | + SExpr::Tensor(x, y) => Tm::Tensor( |
| 152 | + Box::new(elaborate(x, scope)?), |
| 153 | + Box::new(elaborate(y, scope)?), |
| 154 | + ), |
| 155 | + SExpr::LetPair { x, y, pair, body } => { |
| 156 | + let pair = elaborate(pair, scope)?; |
| 157 | + scope.push(x.clone()); // index 1 in body |
| 158 | + scope.push(y.clone()); // index 0 in body (innermost) |
| 159 | + let body = elaborate(body, scope); |
| 160 | + scope.pop(); |
| 161 | + scope.pop(); |
| 162 | + Tm::LetPair(Box::new(pair), Box::new(body?)) |
| 163 | + } |
| 164 | + SExpr::Inl(other, e) => Tm::Inl(elaborate_ty(other), Box::new(elaborate(e, scope)?)), |
| 165 | + SExpr::Inr(other, e) => Tm::Inr(elaborate_ty(other), Box::new(elaborate(e, scope)?)), |
| 166 | + SExpr::Case { scrut, xl, arm_l, xr, arm_r } => { |
| 167 | + let scrut = elaborate(scrut, scope)?; |
| 168 | + scope.push(xl.clone()); |
| 169 | + let arm_l = elaborate(arm_l, scope); |
| 170 | + scope.pop(); |
| 171 | + scope.push(xr.clone()); |
| 172 | + let arm_r = elaborate(arm_r, scope); |
| 173 | + scope.pop(); |
| 174 | + Tm::Case(Box::new(scrut), Box::new(arm_l?), Box::new(arm_r?)) |
| 175 | + } |
| 176 | + SExpr::Let { q, name, value, body } => { |
| 177 | + let value = elaborate(value, scope)?; |
| 178 | + scope.push(name.clone()); |
| 179 | + let body = elaborate(body, scope); |
| 180 | + scope.pop(); |
| 181 | + Tm::Let(*q, Box::new(value), Box::new(body?)) |
| 182 | + } |
| 183 | + SExpr::Echo { mode, a, b, witness } => Tm::MkEcho( |
| 184 | + *mode, |
| 185 | + elaborate_ty(a), |
| 186 | + elaborate_ty(b), |
| 187 | + Box::new(elaborate(witness, scope)?), |
| 188 | + ), |
| 189 | + SExpr::Weaken(e) => Tm::Weaken(Box::new(elaborate(e, scope)?)), |
| 190 | + }) |
| 191 | +} |
| 192 | + |
| 193 | +/// Outcome of resource-checking a closed surface term. |
| 194 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 195 | +pub enum CheckError { |
| 196 | + /// Elaboration failed (e.g. an unbound name). |
| 197 | + Elab(ElabError), |
| 198 | + /// Elaborated, but the verified usage-walk rejected it (ill-typed or a |
| 199 | + /// linear binder dropped/duplicated). |
| 200 | + Untypable, |
| 201 | +} |
| 202 | + |
| 203 | +/// Elaborate a **closed** surface term and run the verified usage-walk. On |
| 204 | +/// success returns its type and exact realised usage (empty for a closed term). |
| 205 | +/// This is the running compiler's hook into the machine-checked QTT discipline. |
| 206 | +pub fn check_surface(e: &SExpr) -> Result<(Ty, Uvec), CheckError> { |
| 207 | + let mut scope = Scope::new(); |
| 208 | + let tm = elaborate(e, &mut scope).map_err(CheckError::Elab)?; |
| 209 | + check(&[], &tm).ok_or(CheckError::Untypable) |
| 210 | +} |
| 211 | + |
| 212 | +#[cfg(test)] |
| 213 | +mod tests { |
| 214 | + use super::*; |
| 215 | + use Q::*; |
| 216 | + |
| 217 | + fn b<T>(x: T) -> Box<T> { |
| 218 | + Box::new(x) |
| 219 | + } |
| 220 | + fn lam(q: Q, p: &str, ty: STy, body: SExpr) -> SExpr { |
| 221 | + SExpr::Lam { q, param: p.into(), ty, body: b(body) } |
| 222 | + } |
| 223 | + fn var(n: &str) -> SExpr { |
| 224 | + SExpr::Var(n.into()) |
| 225 | + } |
| 226 | + |
| 227 | + /// `|1 x : Unit| x` — the linear identity elaborates and checks; the usage |
| 228 | + /// axis is now enforced on a NAMED surface term by the verified walk. |
| 229 | + #[test] |
| 230 | + fn linear_identity_ok() { |
| 231 | + let id = lam(One, "x", STy::Unit, var("x")); |
| 232 | + assert_eq!( |
| 233 | + check_surface(&id), |
| 234 | + Ok((Ty::Arr(One, b(Ty::Unit), b(Ty::Unit)), vec![])) |
| 235 | + ); |
| 236 | + } |
| 237 | + |
| 238 | + /// `|1 x : Unit| unit` — a linear binder dropped at the surface is REJECTED |
| 239 | + /// (the coupling's whole point: the surface inherits the proof's discipline). |
| 240 | + #[test] |
| 241 | + fn linear_drop_rejected() { |
| 242 | + let drop = lam(One, "x", STy::Unit, SExpr::Unit); |
| 243 | + assert_eq!(check_surface(&drop), Err(CheckError::Untypable)); |
| 244 | + } |
| 245 | + |
| 246 | + /// `|ω x : Unit| (x, x)` — the same body under an `ω` binder is accepted, so |
| 247 | + /// the rejection above is exactly the linearity check, not a parse error. |
| 248 | + #[test] |
| 249 | + fn omega_dup_ok() { |
| 250 | + let dup = lam(Omega, "x", STy::Unit, SExpr::Tensor(b(var("x")), b(var("x")))); |
| 251 | + assert!(matches!(check_surface(&dup), Ok((Ty::Arr(Omega, _, _), _)))); |
| 252 | + } |
| 253 | + |
| 254 | + /// Nested binders resolve to the right de Bruijn indices. `|1 x| |1 y| (x, y)` |
| 255 | + /// uses BOTH exactly once → accepted; `|1 x| |1 y| y` drops the outer `x` → |
| 256 | + /// linear-rejected. (Each non-trivial term uses each linear binder exactly |
| 257 | + /// once, so resolution and the usage walk are both exercised.) |
| 258 | + #[test] |
| 259 | + fn scope_resolution() { |
| 260 | + let both = lam( |
| 261 | + One, |
| 262 | + "x", |
| 263 | + STy::Unit, |
| 264 | + lam(One, "y", STy::Unit, SExpr::Tensor(b(var("x")), b(var("y")))), |
| 265 | + ); |
| 266 | + assert!(check_surface(&both).is_ok()); |
| 267 | + let drop_outer = lam(One, "x", STy::Unit, lam(One, "y", STy::Unit, var("y"))); |
| 268 | + assert_eq!(check_surface(&drop_outer), Err(CheckError::Untypable)); |
| 269 | + } |
| 270 | + |
| 271 | + /// `let (1 z) = unit in z` — a linear `let` used once is accepted. |
| 272 | + #[test] |
| 273 | + fn linear_let_ok() { |
| 274 | + let e = SExpr::Let { |
| 275 | + q: One, |
| 276 | + name: "z".into(), |
| 277 | + value: b(SExpr::Unit), |
| 278 | + body: b(var("z")), |
| 279 | + }; |
| 280 | + assert_eq!(check_surface(&e), Ok((Ty::Unit, vec![]))); |
| 281 | + } |
| 282 | + |
| 283 | + /// An unbound name is an elaboration error, distinct from ill-typed. |
| 284 | + #[test] |
| 285 | + fn unbound_is_elab_error() { |
| 286 | + assert_eq!( |
| 287 | + check_surface(&var("nope")), |
| 288 | + Err(CheckError::Elab(ElabError::Unbound("nope".into()))) |
| 289 | + ); |
| 290 | + } |
| 291 | +} |
0 commit comments