Skip to content

Commit fb3db5a

Browse files
hyperpolymathclaude
andcommitted
feat(suggest): implement suggest_tactics for Abella, KeYmaeraX, EasyCrypt (Stage 4c)
Replaces three empty stubs with curated tactic lists fed through gnn_augment_tactics, matching the same pattern as Isabelle/Coq/Lean: - Abella: 14 sequent-calculus HOL tactics (search, induction, backchain, coinduction, cases, exists, unfold, assert, inst, cut, monotone, …) - KeYmaeraX: 15 Bellerophon tactics (auto, QE, ODE, dI, dC, dW, diffInd, loop, solve, prop, implyR, andL, hideL, closeTrue, cut) - EasyCrypt: 15 pRHL/ambient-logic tactics (proc, wp, sp, seq, call, rnd, skip, apply, trivial, smt, split, inline, swap, conseq, hoare) csi.rs: adds comment explaining automated-solver rationale (no change to behaviour). proverif.rs already had the comment. Adds 6 new tests (2 per interactive backend). 1004 lib tests passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 133e31b commit fb3db5a

4 files changed

Lines changed: 146 additions & 13 deletions

File tree

src/rust/provers/abella.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,30 @@ impl ProverBackend for AbellaBackend {
174174
}))
175175
}
176176

177-
async fn suggest_tactics(&self, _state: &ProofState, _limit: usize) -> Result<Vec<Tactic>> {
178-
// Cold-start: no suggestions until the Abella corpus is extracted
179-
// and the ML layer is retrained.
180-
Ok(vec![])
177+
async fn suggest_tactics(&self, state: &ProofState, limit: usize) -> Result<Vec<Tactic>> {
178+
if state.goals.is_empty() {
179+
return Ok(vec![]);
180+
}
181+
// Abella is a sequent-calculus HOL prover. These are the core tactics
182+
// drawn from the Abella manual (v2.x), ordered by how often they appear
183+
// in the distributed examples.
184+
let tactics = vec![
185+
Tactic::Custom { prover: "abella".to_string(), command: "search".to_string(), args: vec![] },
186+
Tactic::Custom { prover: "abella".to_string(), command: "induction".to_string(), args: vec![] },
187+
Tactic::Custom { prover: "abella".to_string(), command: "apply".to_string(), args: vec![] },
188+
Tactic::Custom { prover: "abella".to_string(), command: "backchain".to_string(), args: vec![] },
189+
Tactic::Custom { prover: "abella".to_string(), command: "intros".to_string(), args: vec![] },
190+
Tactic::Custom { prover: "abella".to_string(), command: "split".to_string(), args: vec![] },
191+
Tactic::Custom { prover: "abella".to_string(), command: "cases".to_string(), args: vec![] },
192+
Tactic::Custom { prover: "abella".to_string(), command: "exists".to_string(), args: vec![] },
193+
Tactic::Custom { prover: "abella".to_string(), command: "unfold".to_string(), args: vec![] },
194+
Tactic::Custom { prover: "abella".to_string(), command: "coinduction".to_string(), args: vec![] },
195+
Tactic::Custom { prover: "abella".to_string(), command: "assert".to_string(), args: vec![] },
196+
Tactic::Custom { prover: "abella".to_string(), command: "inst".to_string(), args: vec![] },
197+
Tactic::Custom { prover: "abella".to_string(), command: "cut".to_string(), args: vec![] },
198+
Tactic::Custom { prover: "abella".to_string(), command: "monotone".to_string(), args: vec![] },
199+
];
200+
Ok(crate::provers::gnn_augment_tactics(&self.config, state, "abella", tactics, limit).await)
181201
}
182202

183203
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
@@ -224,4 +244,26 @@ mod tests {
224244
let backend = AbellaBackend::new(ProverConfig::default());
225245
assert_eq!(backend.kind(), ProverKind::Abella);
226246
}
247+
248+
#[tokio::test]
249+
async fn suggest_tactics_empty_goals_returns_empty() {
250+
let backend = AbellaBackend::new(ProverConfig::default());
251+
let state = ProofState::default();
252+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
253+
assert!(tactics.is_empty());
254+
}
255+
256+
#[tokio::test]
257+
async fn suggest_tactics_returns_abella_tactics() {
258+
let backend = AbellaBackend::new(ProverConfig::default());
259+
let src = "Theorem id : forall x, x = x.\nintros. search.\n";
260+
let state = backend.parse_string(src).await.unwrap();
261+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
262+
assert!(!tactics.is_empty());
263+
let names: Vec<_> = tactics.iter().filter_map(|t| {
264+
if let Tactic::Custom { command, .. } = t { Some(command.as_str()) } else { None }
265+
}).collect();
266+
assert!(names.contains(&"search"), "expected 'search' tactic");
267+
assert!(names.contains(&"induction"), "expected 'induction' tactic");
268+
}
227269
}

src/rust/provers/csi.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ impl ProverBackend for CSIBackend {
168168
_state: &ProofState,
169169
_limit: usize,
170170
) -> Result<Vec<Tactic>> {
171+
// CSI is a fully automated TRS confluence/termination solver.
172+
// It has no user-facing tactic language — correctness certificates
173+
// are produced internally. Returning empty is correct behaviour.
171174
Ok(vec![])
172175
}
173176

src/rust/provers/easycrypt.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,33 @@ impl ProverBackend for EasyCryptBackend {
235235

236236
async fn suggest_tactics(
237237
&self,
238-
_state: &ProofState,
239-
_limit: usize,
238+
state: &ProofState,
239+
limit: usize,
240240
) -> Result<Vec<Tactic>> {
241-
Ok(vec![])
241+
if state.goals.is_empty() {
242+
return Ok(vec![]);
243+
}
244+
// EasyCrypt pRHL / ambient-logic tactics. Drawn from the EasyCrypt
245+
// reference manual and standard library proofs. Ordered by frequency
246+
// in the distributed game-based crypto proof corpus.
247+
let tactics = vec![
248+
Tactic::Custom { prover: "easycrypt".to_string(), command: "proc".to_string(), args: vec![] },
249+
Tactic::Custom { prover: "easycrypt".to_string(), command: "wp".to_string(), args: vec![] },
250+
Tactic::Custom { prover: "easycrypt".to_string(), command: "sp".to_string(), args: vec![] },
251+
Tactic::Custom { prover: "easycrypt".to_string(), command: "seq".to_string(), args: vec![] },
252+
Tactic::Custom { prover: "easycrypt".to_string(), command: "call".to_string(), args: vec![] },
253+
Tactic::Custom { prover: "easycrypt".to_string(), command: "rnd".to_string(), args: vec![] },
254+
Tactic::Custom { prover: "easycrypt".to_string(), command: "skip".to_string(), args: vec![] },
255+
Tactic::Custom { prover: "easycrypt".to_string(), command: "apply".to_string(), args: vec![] },
256+
Tactic::Custom { prover: "easycrypt".to_string(), command: "trivial".to_string(), args: vec![] },
257+
Tactic::Custom { prover: "easycrypt".to_string(), command: "smt".to_string(), args: vec![] },
258+
Tactic::Custom { prover: "easycrypt".to_string(), command: "split".to_string(), args: vec![] },
259+
Tactic::Custom { prover: "easycrypt".to_string(), command: "inline".to_string(), args: vec![] },
260+
Tactic::Custom { prover: "easycrypt".to_string(), command: "swap".to_string(), args: vec![] },
261+
Tactic::Custom { prover: "easycrypt".to_string(), command: "conseq".to_string(), args: vec![] },
262+
Tactic::Custom { prover: "easycrypt".to_string(), command: "hoare".to_string(), args: vec![] },
263+
];
264+
Ok(crate::provers::gnn_augment_tactics(&self.config, state, "easycrypt", tactics, limit).await)
242265
}
243266

244267
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
@@ -311,4 +334,26 @@ mod tests {
311334
assert_eq!(state.context.axioms.len(), 1);
312335
assert_eq!(state.goals.len(), 1);
313336
}
337+
338+
#[tokio::test]
339+
async fn suggest_tactics_empty_goals_returns_empty() {
340+
let backend = EasyCryptBackend::new(ProverConfig::default());
341+
let state = ProofState::default();
342+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
343+
assert!(tactics.is_empty());
344+
}
345+
346+
#[tokio::test]
347+
async fn suggest_tactics_returns_ec_tactics() {
348+
let backend = EasyCryptBackend::new(ProverConfig::default());
349+
let ec = "lemma triv : forall x, x = x.\n";
350+
let state = backend.parse_string(ec).await.expect("parse_string");
351+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
352+
assert!(!tactics.is_empty());
353+
let names: Vec<_> = tactics.iter().filter_map(|t| {
354+
if let Tactic::Custom { command, .. } = t { Some(command.as_str()) } else { None }
355+
}).collect();
356+
assert!(names.contains(&"wp"), "expected 'wp' tactic");
357+
assert!(names.contains(&"rnd"), "expected 'rnd' tactic");
358+
}
314359
}

src/rust/provers/keymaerax.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,34 @@ impl ProverBackend for KeYmaeraXBackend {
285285

286286
async fn suggest_tactics(
287287
&self,
288-
_state: &ProofState,
289-
_limit: usize,
288+
state: &ProofState,
289+
limit: usize,
290290
) -> Result<Vec<Tactic>> {
291-
// The Bellerophon tactic language is too large to enumerate
292-
// statically here; GNN-ranked Bellerophon suggestion is
293-
// queued under §4.4 of ECHIDNA-NOTES.
294-
Ok(vec![])
291+
if state.goals.is_empty() {
292+
return Ok(vec![]);
293+
}
294+
// KeYmaera X Bellerophon tactics. This is a curated starter set
295+
// covering the tactics most visible in the KeYmaera X tutorial and
296+
// distributed benchmark archive. The full Bellerophon language is
297+
// open-ended; GNN-ranked corpus suggestions (§4.4) will extend this.
298+
let tactics = vec![
299+
Tactic::Custom { prover: "keymaerax".to_string(), command: "auto".to_string(), args: vec![] },
300+
Tactic::Custom { prover: "keymaerax".to_string(), command: "QE".to_string(), args: vec![] },
301+
Tactic::Custom { prover: "keymaerax".to_string(), command: "loop".to_string(), args: vec![] },
302+
Tactic::Custom { prover: "keymaerax".to_string(), command: "ODE".to_string(), args: vec![] },
303+
Tactic::Custom { prover: "keymaerax".to_string(), command: "solve".to_string(), args: vec![] },
304+
Tactic::Custom { prover: "keymaerax".to_string(), command: "dI".to_string(), args: vec![] },
305+
Tactic::Custom { prover: "keymaerax".to_string(), command: "dC".to_string(), args: vec![] },
306+
Tactic::Custom { prover: "keymaerax".to_string(), command: "dW".to_string(), args: vec![] },
307+
Tactic::Custom { prover: "keymaerax".to_string(), command: "diffInd".to_string(), args: vec![] },
308+
Tactic::Custom { prover: "keymaerax".to_string(), command: "cut".to_string(), args: vec![] },
309+
Tactic::Custom { prover: "keymaerax".to_string(), command: "prop".to_string(), args: vec![] },
310+
Tactic::Custom { prover: "keymaerax".to_string(), command: "implyR".to_string(), args: vec![] },
311+
Tactic::Custom { prover: "keymaerax".to_string(), command: "andL".to_string(), args: vec![] },
312+
Tactic::Custom { prover: "keymaerax".to_string(), command: "hideL".to_string(), args: vec![] },
313+
Tactic::Custom { prover: "keymaerax".to_string(), command: "closeTrue".to_string(), args: vec![] },
314+
];
315+
Ok(crate::provers::gnn_augment_tactics(&self.config, state, "keymaerax", tactics, limit).await)
295316
}
296317

297318
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
@@ -405,4 +426,26 @@ mod tests {
405426
_ => panic!("expected Term::Const"),
406427
}
407428
}
429+
430+
#[tokio::test]
431+
async fn suggest_tactics_empty_goals_returns_empty() {
432+
let backend = KeYmaeraXBackend::new(ProverConfig::default());
433+
let state = ProofState::default();
434+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
435+
assert!(tactics.is_empty());
436+
}
437+
438+
#[tokio::test]
439+
async fn suggest_tactics_returns_kyx_tactics() {
440+
let backend = KeYmaeraXBackend::new(ProverConfig::default());
441+
let kyx = "ArchiveEntry \"x\"\n Problem\n x > 0 -> [x := x + 1;] x > 0\n End.\nEnd.\n";
442+
let state = backend.parse_string(kyx).await.expect("parse_string");
443+
let tactics = backend.suggest_tactics(&state, 10).await.unwrap();
444+
assert!(!tactics.is_empty());
445+
let names: Vec<_> = tactics.iter().filter_map(|t| {
446+
if let Tactic::Custom { command, .. } = t { Some(command.as_str()) } else { None }
447+
}).collect();
448+
assert!(names.contains(&"auto"), "expected 'auto' tactic");
449+
assert!(names.contains(&"QE"), "expected 'QE' tactic");
450+
}
408451
}

0 commit comments

Comments
 (0)