Skip to content

Commit d6b8f56

Browse files
committed
feat(my-qtt/surface): named-variable elaboration engine onto the QTT core (coupling step 2a)
The bridge from a named surface to the de Bruijn core the verified check consumes, so the resource axis can be enforced on real programs. SExpr/STy = the resource-relevant fragment of my-lang's ast::Expr/ast::Type (var, λ-with- quantity, app, both products, sums, let, echo); elaborate resolves names to de Bruijn against a Scope (innermost last, matching Tctx); check_surface runs the machine-checked usage-walk on the elaborated term. 6 tests green: a linear binder DROPPED at the named surface is rejected by the verified algorithm (linear_drop_rejected), the same body under ω is accepted (omega_dup_ok), nested binders resolve to correct indices (scope_resolution), linear let, and unbound-name elaboration errors. Quantities are explicit on SExpr binders, so the ast lowering (step 2b) can default them today and thread real linearity syntax later with no engine change.
1 parent f2eff3b commit d6b8f56

2 files changed

Lines changed: 293 additions & 0 deletions

File tree

crates/my-qtt/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
3636
#![forbid(unsafe_code)]
3737

38+
pub mod surface;
39+
3840
/// Quantities: the three-point affine semiring `{0, 1, ω}` (Coq `Quantity.Q`).
3941
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4042
pub enum Q {

crates/my-qtt/src/surface.rs

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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

Comments
 (0)