|
| 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