Skip to content

Commit 48e5a86

Browse files
committed
feat(my-lang/qtt_bridge): lower real ast::Expr onto the verified QTT core (coupling step 2b)
my-qtt now sits in my-lang's dependency graph — the proofs' own usage-walk is in the compiler. qtt_bridge lowers the resource-relevant fragment of the running compiler's ast::Expr/ast::Type (var, λ, application, let/block, literal) onto my_qtt::surface, and check_expr runs the machine-checked check on it. Honest scope: opaque base types -> Unit; -> and tuples map structurally; out-of-fragment constructs (binary/unary/match/record/AI/try/restrict) return BridgeError::Unsupported (no silent passes). Quantity default = One (linear), since Param has no quantity syntax yet and the Coq check is exact-usage — so the bridge ENFORCES 'use each binder exactly once' on the real AST: a real |x| x is accepted, a real |x| { 0 } (drops x) is rejected by the verified walk. Surface quantity syntax (small follow-on) activates real per-binder quantities with no engine change. 6 tests green over real ast::Expr nodes. With step 2a (the engine), item 1 (coupling step 2) is complete: proofs -> implementation is wired and enforcing.
1 parent d6b8f56 commit 48e5a86

3 files changed

Lines changed: 263 additions & 0 deletions

File tree

crates/my-lang/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ name = "my"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19+
my-qtt.workspace = true
1920
thiserror.workspace = true
2021
anyhow.workspace = true
2122
serde = { workspace = true, optional = true }

crates/my-lang/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod scope;
2323
pub mod stdlib;
2424
pub mod token;
2525
pub mod types;
26+
pub mod qtt_bridge;
2627

2728
// Library modules (common utilities and language-specific features)
2829
#[path = "../lib/mod.rs"]

crates/my-lang/src/qtt_bridge.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
4+
//! # `qtt_bridge` — lower the my-lang surface AST onto the verified QTT core
5+
//!
6+
//! Coupling **step 2b**: the actual lowering of the running compiler's
7+
//! [`crate::ast::Expr`]/[`crate::ast::Type`] onto [`my_qtt::surface::SExpr`], so
8+
//! that the machine-checked QTT usage-walk (`my-qtt`, the faithful port of the
9+
//! Coq `check`) can be run on real my-lang programs. With `my-qtt` now in
10+
//! my-lang's dependency graph, the proofs' own algorithm is *in the compiler*.
11+
//!
12+
//! ## Scope (honest)
13+
//! Only the **resource-relevant fragment** lowers: variables, λ, application,
14+
//! `let`/block bindings, and literals (as opaque values). Everything else
15+
//! (binary/unary ops, `match`, records, AI expressions, `try`, `restrict`, …)
16+
//! returns [`BridgeError::Unsupported`] — these are follow-on work, not silent
17+
//! passes. Base/opaque types (`Int`/`String`/…/records/effects) carry no
18+
//! resource content and map to `Unit`; function and tuple types map
19+
//! structurally (`->` to the affine arrow, tuples to `⊗`).
20+
//!
21+
//! ## The quantity default
22+
//! The Coq `check` is **exact-usage**: a binder's declared quantity must equal
23+
//! its realised usage. The my-lang `Param` has no quantity syntax yet, so this
24+
//! bridge applies the **linear** reading ([`DEFAULT_Q`]` = One`): every binder
25+
//! must be used *exactly once*. That is the strictest QTT discipline — it makes
26+
//! [`check_expr`] genuinely *enforce* linearity on the real AST (a dropped or
27+
//! duplicated binder is rejected by the verified walk), and it is the most
28+
//! faithful demonstration of the coupling. Real per-binder quantities await
29+
//! `Param` gaining quantity annotations (a small parser+AST follow-on); the
30+
//! engine already threads whatever quantity it is given, so that change
31+
//! activates here with no rewrite.
32+
33+
use crate::ast::{Block, Expr, LambdaBody, Stmt, Type};
34+
use my_qtt::surface::{check_surface, CheckError, SExpr, STy};
35+
use my_qtt::{Q, Ty, Uvec};
36+
37+
/// The quantity assigned to every surface binder pending quantity syntax — the
38+
/// linear reading (`One` = used exactly once). See the module docs.
39+
pub const DEFAULT_Q: Q = Q::One;
40+
41+
/// A surface construct outside the resource-relevant fragment.
42+
#[derive(Debug, Clone, PartialEq, Eq)]
43+
pub enum BridgeError {
44+
Unsupported(&'static str),
45+
}
46+
47+
/// Lower a surface type to a core resource type. Opaque base types map to
48+
/// `Unit` (no resource content); `->` and tuples map structurally.
49+
pub fn lower_ty(t: &Type) -> STy {
50+
match t {
51+
Type::Function { param, result, .. } => {
52+
STy::Arr(DEFAULT_Q, Box::new(lower_ty(param)), Box::new(lower_ty(result)))
53+
}
54+
Type::Tuple { elements, .. } => lower_tuple(elements),
55+
Type::Reference { inner, .. } => lower_ty(inner),
56+
// Primitive / Named / Effect / Ai / Array / Record / Constrained: opaque.
57+
_ => STy::Unit,
58+
}
59+
}
60+
61+
fn lower_tuple(elems: &[Type]) -> STy {
62+
match elems {
63+
[] => STy::Unit,
64+
[t] => lower_ty(t),
65+
[t, rest @ ..] => STy::Tensor(Box::new(lower_ty(t)), Box::new(lower_tuple(rest))),
66+
}
67+
}
68+
69+
/// Lower a surface expression to the named-variable core fragment.
70+
pub fn lower_expr(e: &Expr) -> Result<SExpr, BridgeError> {
71+
Ok(match e {
72+
Expr::Ident(id) => SExpr::Var(id.name.clone()),
73+
// a literal is a value with no resource content.
74+
Expr::Literal(_) => SExpr::Unit,
75+
Expr::Lambda { params, body, .. } => {
76+
let inner = match body {
77+
LambdaBody::Expr(e) => lower_expr(e)?,
78+
LambdaBody::Block(b) => lower_block(b)?,
79+
};
80+
// right-fold params: `|x, y| body` -> `Lam x (Lam y body)`
81+
let mut acc = inner;
82+
for p in params.iter().rev() {
83+
acc = SExpr::Lam {
84+
q: DEFAULT_Q,
85+
param: p.name.name.clone(),
86+
ty: lower_ty(&p.ty),
87+
body: Box::new(acc),
88+
};
89+
}
90+
acc
91+
}
92+
Expr::Call { callee, args, .. } => {
93+
// `f(a, b)` -> `App (App f a) b`
94+
let mut acc = lower_expr(callee)?;
95+
for a in args {
96+
acc = SExpr::App(Box::new(acc), Box::new(lower_expr(a)?));
97+
}
98+
acc
99+
}
100+
Expr::Block(b) => lower_block(b)?,
101+
Expr::Binary { .. } => return Err(BridgeError::Unsupported("binary op")),
102+
Expr::Unary { .. } => return Err(BridgeError::Unsupported("unary op")),
103+
Expr::Field { .. } => return Err(BridgeError::Unsupported("field access")),
104+
Expr::Try { .. } => return Err(BridgeError::Unsupported("try")),
105+
Expr::Restrict { .. } => return Err(BridgeError::Unsupported("restrict")),
106+
Expr::Ai(_) => return Err(BridgeError::Unsupported("AI expression")),
107+
Expr::Match { .. } => return Err(BridgeError::Unsupported("match (use a 2-arm sum case)")),
108+
Expr::Array { .. } => return Err(BridgeError::Unsupported("array literal")),
109+
Expr::Record { .. } => return Err(BridgeError::Unsupported("record literal")),
110+
})
111+
}
112+
113+
/// Lower a block: `let` statements become nested core `let`s; a trailing
114+
/// expression statement is the block's value (an empty block is `unit`). Other
115+
/// statement forms (control flow, non-final non-`let` statements) are out of
116+
/// the resource fragment.
117+
pub fn lower_block(b: &Block) -> Result<SExpr, BridgeError> {
118+
lower_stmts(&b.stmts)
119+
}
120+
121+
fn lower_stmts(stmts: &[Stmt]) -> Result<SExpr, BridgeError> {
122+
match stmts {
123+
[] => Ok(SExpr::Unit),
124+
[Stmt::Expr(e)] => lower_expr(e),
125+
[Stmt::Let { name, value, .. }, rest @ ..] => Ok(SExpr::Let {
126+
q: DEFAULT_Q,
127+
name: name.name.clone(),
128+
value: Box::new(lower_expr(value)?),
129+
body: Box::new(lower_stmts(rest)?),
130+
}),
131+
_ => Err(BridgeError::Unsupported("block statement form")),
132+
}
133+
}
134+
135+
/// Lowering or checking failure.
136+
#[derive(Debug, Clone, PartialEq, Eq)]
137+
pub enum BridgeCheckError {
138+
/// The surface construct is outside the resource fragment.
139+
Bridge(BridgeError),
140+
/// Lowered, but the verified usage-walk rejected it (ill-typed, or — under
141+
/// the linear default — a binder dropped or duplicated).
142+
Check(CheckError),
143+
}
144+
145+
/// Lower a surface expression and run the **machine-checked** QTT usage-walk on
146+
/// it. The running compiler's hook into the proofs' own resource discipline.
147+
pub fn check_expr(e: &Expr) -> Result<(Ty, Uvec), BridgeCheckError> {
148+
let s = lower_expr(e).map_err(BridgeCheckError::Bridge)?;
149+
check_surface(&s).map_err(BridgeCheckError::Check)
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use super::*;
155+
use crate::ast::{Expr, Ident, Literal, Param, PrimitiveType};
156+
use crate::token::Span;
157+
use my_qtt::surface::ElabError;
158+
159+
fn sp() -> Span {
160+
Span { start: 0, end: 0, line: 0, column: 0 }
161+
}
162+
fn id(n: &str) -> Ident {
163+
Ident::new(n, sp())
164+
}
165+
fn param(n: &str, ty: Type) -> Param {
166+
Param { name: id(n), ty, span: sp() }
167+
}
168+
169+
/// A real `ast::Expr::Lambda` `|x: Int| x` lowers and is accepted: the
170+
/// linear binder is used exactly once. The verified engine now runs on the
171+
/// compiler's actual AST.
172+
#[test]
173+
fn real_linear_lambda_accepted() {
174+
let lam = Expr::Lambda {
175+
params: vec![param("x", Type::Primitive(PrimitiveType::Int))],
176+
body: LambdaBody::Expr(Box::new(Expr::Ident(id("x")))),
177+
span: sp(),
178+
};
179+
let r = check_expr(&lam);
180+
assert!(r.is_ok(), "expected ok, got {:?}", r);
181+
// Int is opaque (Unit); the arrow carries the linear quantity.
182+
assert!(matches!(r.unwrap().0, Ty::Arr(Q::One, _, _)));
183+
}
184+
185+
/// `|x: Int| { 0 }` drops the linear binder `x` → rejected by the verified
186+
/// walk. Linearity is enforced on the real AST.
187+
#[test]
188+
fn real_dropping_lambda_rejected() {
189+
let lam = Expr::Lambda {
190+
params: vec![param("x", Type::Primitive(PrimitiveType::Int))],
191+
body: LambdaBody::Block(Block {
192+
stmts: vec![Stmt::Expr(Expr::Literal(Literal::Int(0, sp())))],
193+
span: sp(),
194+
}),
195+
span: sp(),
196+
};
197+
assert_eq!(
198+
check_expr(&lam),
199+
Err(BridgeCheckError::Check(CheckError::Untypable))
200+
);
201+
}
202+
203+
/// A block `{ let z = 0; z }` lowers to a core `let` and checks (z used once).
204+
#[test]
205+
fn real_block_let_accepted() {
206+
let blk = Expr::Block(Block {
207+
stmts: vec![
208+
Stmt::Let {
209+
mutable: false,
210+
name: id("z"),
211+
ty: None,
212+
value: Expr::Literal(Literal::Int(0, sp())),
213+
span: sp(),
214+
},
215+
Stmt::Expr(Expr::Ident(id("z"))),
216+
],
217+
span: sp(),
218+
});
219+
assert!(check_expr(&blk).is_ok());
220+
}
221+
222+
/// A free identifier is an elaboration error, surfaced through the bridge.
223+
#[test]
224+
fn real_unbound_ident() {
225+
assert_eq!(
226+
check_expr(&Expr::Ident(id("free"))),
227+
Err(BridgeCheckError::Check(CheckError::Elab(ElabError::Unbound(
228+
"free".into()
229+
))))
230+
);
231+
}
232+
233+
/// Out-of-fragment constructs are reported, not silently accepted.
234+
#[test]
235+
fn unsupported_is_reported() {
236+
let field = Expr::Field {
237+
object: Box::new(Expr::Ident(id("r"))),
238+
field: id("f"),
239+
span: sp(),
240+
};
241+
assert_eq!(
242+
check_expr(&field),
243+
Err(BridgeCheckError::Bridge(BridgeError::Unsupported("field access")))
244+
);
245+
}
246+
247+
/// Function and tuple types lower structurally; base types are opaque `Unit`.
248+
#[test]
249+
fn type_lowering() {
250+
assert_eq!(lower_ty(&Type::Primitive(PrimitiveType::Int)), STy::Unit);
251+
let f = Type::Function {
252+
param: Box::new(Type::Primitive(PrimitiveType::Int)),
253+
result: Box::new(Type::Primitive(PrimitiveType::Bool)),
254+
span: sp(),
255+
};
256+
assert_eq!(
257+
lower_ty(&f),
258+
STy::Arr(Q::One, Box::new(STy::Unit), Box::new(STy::Unit))
259+
);
260+
}
261+
}

0 commit comments

Comments
 (0)