Skip to content

Commit 7cd7b64

Browse files
committed
feat(checker): enforce the verified QTT resource discipline on #[safe] fns
Wires the machine-checked QTT core (my_qtt::check, the faithful R5 port) into the compiler's default check path — closing the docs/STATUS.adoc "make the resource axis the default in checker.rs" item. `Checker::check_function` now, for every function annotated `#[safe]`, models it as `|params| body` and runs `qtt_bridge::check_expr` (→ verified `my_qtt::check`). A parameter dropped or used more than once WITHIN the resource-relevant fragment is reported as `CheckError::ResourceViolation`; a body using constructs outside that fragment (arithmetic, records, AI exprs, calls to globals, `return`) cannot be lowered and is skipped — unverifiable is not unsafe. Runs by default (no flag); gated on `#[safe]` so the rest of the language (which is not linear-by-default) is unaffected. End-to-end via `typecheck`: #[safe] fn id(x: Int) { x; } -> OK (x used once) #[safe] fn drp(x: Int) { 0; } -> ResourceViolation (x dropped) fn drp(x: Int) { 0; } -> OK (not #[safe], not enforced) #[safe] fn add1(x: Int){ x + 1; } -> OK (arithmetic out-of-fragment, skipped) 4 new checker tests; all 153 my-lang tests green. STATUS docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwV2DWsjkBiNP3oscimMLV
1 parent 1a24a42 commit 7cd7b64

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

crates/my-lang/src/checker.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ pub enum CheckError {
109109
line: usize,
110110
column: usize,
111111
},
112+
113+
#[error("`@safe` function `{name}` violates the verified linear resource discipline (a parameter is dropped or used more than once) at line {line}, column {column}")]
114+
ResourceViolation {
115+
name: String,
116+
line: usize,
117+
column: usize,
118+
},
112119
}
113120

114121
/// Maximum AST nesting depth the type checker will recurse through before
@@ -614,10 +621,56 @@ impl Checker {
614621
// Check function body
615622
self.check_block(&f.body);
616623

624+
// `@safe`: enforce the machine-checked QTT resource discipline (runs by
625+
// default — no flag — for every `@safe`-annotated function).
626+
if f.modifiers.contains(&FnModifier::Safe) {
627+
self.check_qtt_safe_fn(f);
628+
}
629+
617630
self.current_return_type = None;
618631
self.symbols.exit_scope();
619632
}
620633

634+
/// Resource-check a `@safe` function with the verified QTT core
635+
/// (`my_qtt::check`, the faithful R5 port reached via [`crate::qtt_bridge`]).
636+
///
637+
/// The function is modelled as a lambda over its parameters — each treated
638+
/// **linearly** (`DEFAULT_Q = One`, the strictest reading) — and the
639+
/// machine-checked usage-walk runs on it. A parameter dropped or used more
640+
/// than once *within the resource-relevant fragment* is reported as a
641+
/// [`CheckError::ResourceViolation`]. A body that uses constructs OUTSIDE
642+
/// that fragment (arithmetic, records, AI expressions, calls to globals, …)
643+
/// cannot be lowered and is **skipped** — unverifiable is not unsafe, and
644+
/// name/type resolution of those parts is the main checker's job. So
645+
/// `@safe` means "the proof core has checked this function's resource
646+
/// discipline wherever it is expressible", enforced by default.
647+
fn check_qtt_safe_fn(&mut self, f: &FnDecl) {
648+
use crate::qtt_bridge::{check_expr, BridgeCheckError};
649+
use my_qtt::surface::CheckError as QttError;
650+
651+
// model the @safe function as `|params| body`
652+
let lam = Expr::Lambda {
653+
params: f.params.clone(),
654+
body: LambdaBody::Block(f.body.clone()),
655+
span: f.span,
656+
};
657+
match check_expr(&lam) {
658+
// resource-safe, or outside the verifiable fragment (skip), or a
659+
// free name the main resolver already diagnoses (skip).
660+
Ok(_)
661+
| Err(BridgeCheckError::Bridge(_))
662+
| Err(BridgeCheckError::Check(QttError::Elab(_))) => {}
663+
// lowered fully, but the verified walk rejected the usage discipline.
664+
Err(BridgeCheckError::Check(QttError::Untypable)) => {
665+
self.errors.push(CheckError::ResourceViolation {
666+
name: f.name.name.clone(),
667+
line: f.span.line,
668+
column: f.span.column,
669+
});
670+
}
671+
}
672+
}
673+
621674
fn check_struct(&mut self, s: &StructDecl) {
622675
// Check that field types are valid
623676
for field in &s.fields {
@@ -1827,4 +1880,45 @@ mod tests {
18271880
let errors = result.unwrap_err();
18281881
assert!(errors.iter().any(|e| matches!(e, CheckError::NonBoolCondition { .. })));
18291882
}
1883+
1884+
// ---- `@safe` functions are resource-checked by the verified QTT core ----
1885+
1886+
/// A `#[safe]` function whose parameter is used exactly once passes the
1887+
/// machine-checked linear discipline.
1888+
#[test]
1889+
fn qtt_safe_linear_param_ok() {
1890+
let result = check_source("#[safe]\nfn id(x: Int) { x; }");
1891+
assert!(result.is_ok(), "expected ok, got {:?}", result);
1892+
}
1893+
1894+
/// A `#[safe]` function that DROPS its linear parameter is rejected by the
1895+
/// verified usage-walk — the resource axis is now enforced in the compiler.
1896+
#[test]
1897+
fn qtt_safe_dropped_param_rejected() {
1898+
let result = check_source("#[safe]\nfn drp(x: Int) { 0; }");
1899+
assert!(result.is_err(), "expected ResourceViolation, got Ok");
1900+
let errors = result.unwrap_err();
1901+
assert!(
1902+
errors.iter().any(|e| matches!(e, CheckError::ResourceViolation { .. })),
1903+
"expected ResourceViolation, got {:?}",
1904+
errors
1905+
);
1906+
}
1907+
1908+
/// The SAME body without `#[safe]` is accepted — the discipline is opt-in
1909+
/// per function (gated on the modifier), so nothing else changes.
1910+
#[test]
1911+
fn non_safe_dropped_param_ok() {
1912+
let result = check_source("fn drp(x: Int) { 0; }");
1913+
assert!(result.is_ok(), "non-@safe fn must not be resource-checked, got {:?}", result);
1914+
}
1915+
1916+
/// A `#[safe]` body that uses constructs OUTSIDE the resource fragment
1917+
/// (here, arithmetic) cannot be lowered and is skipped — unverifiable is
1918+
/// not unsafe, so no false rejection.
1919+
#[test]
1920+
fn qtt_safe_out_of_fragment_skipped() {
1921+
let result = check_source("#[safe]\nfn add1(x: Int) { x + 1; }");
1922+
assert!(result.is_ok(), "out-of-fragment @safe body should be skipped, got {:?}", result);
1923+
}
18301924
}

docs/STATUS.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ the Idris twin's `preservation` (#108) is now DISCHARGED (total, hole-free;
4545
[cols="4,1,3",options="header"]
4646
|===
4747
| Item | Status | Note
48-
| Make the resource axis the *default* in `crates/my-lang/src/checker.rs` | `[ ]` | the QTT bridge is opt-in (`qtt_bridge::check_expr`) today
48+
| Make the resource axis the *default* in `crates/my-lang/src/checker.rs` | `[x]` | DONE — the verified QTT core runs by default (no flag) on every `#[safe]` function: `Checker::check_qtt_safe_fn` lowers it via `qtt_bridge` and a dropped/duplicated binder in the resource fragment is a `CheckError::ResourceViolation`; out-of-fragment bodies skip (unverifiable ≠ unsafe). Gated on `#[safe]` so non-linear code is unaffected.
4949
| Surface *quantity syntax* so the bridge enforces real per-binder linearity | `[ ]` | `Param` has no quantity field; bridge defaults to `One`
5050
| `wasm32` via the existing LLVM backend (`TargetSpec::wasm32` + `wasm-ld`) | `[ ]` | fastest target leg; backend already emits objects
5151
| `RISC-V` (`riscv64gc-unknown-linux-gnu`) via LLVM | `[ ]` | near-free once `wasm32` target wiring exists

proofs/STATUS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ verified against the crate sources + the conformance harness.)
122122

123123
| Obligation | Status | Note |
124124
|------------|--------|------|
125-
| Rust affine checker refines Coq `check` / `aff_type_dec` (R5/R5b) | **conformance-checked** | **CLOSED by differential conformance 2026-06-21.** The verified Coq `check` (R5) is extracted to OCaml (`proofs/verification/coq/solo-core/Extract.v`, `Separate Extraction check`) and used as an independent ORACLE; the Rust port `crates/my-qtt` is the implementation under test. `conformance/run.sh` generates a random corpus of `(ctx, tm)` queries (`crates/my-qtt/src/bin/conformance_gen.rs`), runs BOTH the extracted verified `check` (`conformance/oracle.ml`) and `my_qtt::check`, and asserts byte-identical results — same accept/reject, same synthesized type, same usage vector — including echo (`MkEcho`/`Weaken`/`TEcho`) terms. **25 000 random terms across 5 seeds agree** (≈13% accept / 87% reject, so both branches are exercised). `my_qtt::aff_check` mirrors the Coq `aff_type_iff` (`check … = Some(a,D0) ∧ ule D0 budget`) and carries the R5/R5b reflexivity oracle tests. SCOPE/honesty: this is differential refinement-by-conformance against the *extracted verified algorithm*, NOT a full Rust-in-Coq refinement proof; and the *main* `crates/my-lang/src/checker.rs` (1830 LOC, Hindley) still does not yet CALL the verified core (`my_qtt` is wired via `qtt_bridge` but opt-in) — making it the default checker is the separate `docs/STATUS.adoc` intend "make the resource axis the default in `checker.rs`". |
125+
| Rust affine checker refines Coq `check` / `aff_type_dec` (R5/R5b) | **conformance-checked** | **CLOSED by differential conformance 2026-06-21.** The verified Coq `check` (R5) is extracted to OCaml (`proofs/verification/coq/solo-core/Extract.v`, `Separate Extraction check`) and used as an independent ORACLE; the Rust port `crates/my-qtt` is the implementation under test. `conformance/run.sh` generates a random corpus of `(ctx, tm)` queries (`crates/my-qtt/src/bin/conformance_gen.rs`), runs BOTH the extracted verified `check` (`conformance/oracle.ml`) and `my_qtt::check`, and asserts byte-identical results — same accept/reject, same synthesized type, same usage vector — including echo (`MkEcho`/`Weaken`/`TEcho`) terms. **25 000 random terms across 5 seeds agree** (≈13% accept / 87% reject, so both branches are exercised). `my_qtt::aff_check` mirrors the Coq `aff_type_iff` (`check … = Some(a,D0) ∧ ule D0 budget`) and carries the R5/R5b reflexivity oracle tests. SCOPE/honesty: this is differential refinement-by-conformance against the *extracted verified algorithm*, NOT a full Rust-in-Coq refinement proof; and the *main* `crates/my-lang/src/checker.rs` (Hindley) now CALLS the verified core by default for `#[safe]` functions (`Checker::check_qtt_safe_fn` → `qtt_bridge` → `my_qtt::check`; a dropped/duplicated binder in the resource fragment is a `CheckError::ResourceViolation`, out-of-fragment bodies skip). So the resource axis is enforced-by-default where opted in via `#[safe]` (the `docs/STATUS.adoc` "make the resource axis the default" item — DONE); whole-program linear-by-default is intentionally NOT the design (my-lang is not a linear language). |
126126
| Evaluator refines Coq `step` (solo core) | **conformance-checked** | **CLOSED 2026-06-21.** The reference semantics is the `step` RELATION (not extractable), so `Eval.v` adds a FUNCTIONAL `step1 : tm -> option tm` and proves it both ways vs the relation — `step1_sound : step1 t = Some t' -> step t t'` and `step1_complete : step t t' -> step1 t = Some t'` — real `Qed`, **axiom-free** (`Print Assumptions` closed), machine-checked in CI (`Eval.v` is in `_CoqProject`). `step1` is extracted alongside `check` (Extract.v) and the Rust port `my_qtt::step1` (faithful `shift`/`subst_at`/`subst0`/`subst2`/`is_value`/`step1`) is differentially conformance-tested against it: `conformance/run.sh` compares **one-step** results AND **iterated normal forms** (cap 64) over a random + closed-redex corpus. **45 000 results across 5 seeds agree** (3000 terms × {check, one-step, normal-form}); the normal-form pass cross-checks the Rust substitution against Coq's extracted `subst`. SCOPE/honesty: this couples an evaluator over the SHARED solo `tm` (the same approach as the checker), exactly mirroring `step`; the separate AI-surface `interpreter.rs` (richer term language, runtime no-ops for `Restrict`/`Ref`/`RefMut`) is NOT what is coupled here and remains an independent, unverified frontend. |
127127
| Echo modality wired into surface + checker | **conformance-checked** (verified-core surface) | **2026-06-21.** Echo is wired end-to-end through the VERIFIED core: the `my_qtt::surface` fragment has echo intro (`SExpr::Echo` / `STy::Echo`) and the `weaken` elimination (`SExpr::Weaken`), `elaborate` lowers them to `Tm::MkEcho`/`Tm::Weaken`, and the machine-checked `check` consumes them — so the `linear ⊑ affine` discipline (`EchoLinear`) is enforced from a surface term by the proof. Pinned by `surface.rs` tests: intro types correctly, a LINEAR echo `weaken`s to AFFINE, weakening an AFFINE echo is REJECTED (the one-way *no-section* fact), and witness linearity is threaded (an echo whose witness drops a linear binder is rejected). Echo terms are also in the random differential-conformance corpus (#1/#2), so `my_qtt`'s echo handling matches the extracted Coq `check`/`step1`. SCOPE/honesty: this closes echo on the verified-core-facing surface; the *conventional compiler* (`crates/my-lang` `ast::Expr`/parser) still has no echo SYNTAX (so `qtt_bridge` cannot yet lower a compiler-AST echo — that needs a type-annotated echo grammar, a frontend follow-on like the `Param` quantity syntax). `types.rs` `EchoMode` is no longer inert at the verified boundary; it is in the conventional frontend. |
128128
| Session runtime refining `cstep` (binary fused config) | **conformance-checked** (binary fragment) | **2026-06-21.** `SessionEval.v` adds a functional `cstep1 : config -> option config` for the binary fused `(νc)(P∣Q)` form and proves it SOUND + COMPLETE vs the `cstep` RELATION (`cstep1_sound`/`cstep1_complete`), real `Qed`, **axiom-free**, in `_CoqProject` (CI-gated). `cstep1` is extracted (Extract.v) and the Rust session runtime `my_qtt::session` (faithful `val`/`party`/`pbranch`/`config` + `vlift`/`vsubst`/`psubst_party`/`open_party`/`pget`/`cstep1`) is differentially conformance-tested against it (`conformance/run.sh`, `(cstep CONFIG)` queries) — **15 000 session steps across 5 seeds agree** (the comm + select redexes and the stuck/None cases). SCOPE/honesty: this couples the BINARY fused config (S1.1b/S1.2 fragment); the n-ary located `nstep` and global `gstep` are NOT yet given executable runtimes (a larger follow-on), and the AI-surface `interpreter.rs` (`Go`/`Await` collapse to sequential) is a separate unverified frontend. |

0 commit comments

Comments
 (0)