Skip to content

Commit dec54bf

Browse files
hyperpolymathclaude
andcommitted
feat(provers): close Phase 3 — 6 real-algebraic + connection-method + tableau backends
Lands all 6 remaining Phase 3 backends in a single bundle, completing the phase that was reserved for solo Opus implementation per docs/handover/PHASE-3-PROMPT.md and ECHIDNA-EXPANSION-TOMORROW-2026-04-28. Real-algebraic decision (CAD / virtual substitution): - src/rust/provers/qepcad.rs (FFI 129) — Cylindrical Algebraic Decomposition; prefixed-formula input with variable-order header; scans for "An equivalent quantifier-free formula:" + TRUE/FALSE/ residual; trust tier 2, complexity 4. - src/rust/provers/redlog.rs (FFI 130) — REDUCE-CAS frontend (rlqe); load_package redlog → rlset reals → rlqe formula pipeline; strips "N: " line-prefix; trust tier 2, complexity 3. Connection-method family (Prolog-based; shared TPTP I/O + SZS parser): - src/rust/provers/connection_method.rs — shared `to_tptp` + `parse_szs_or_native` helpers (6 unit tests covering both SZS and native "matrix proof found" paths). - src/rust/provers/mleancop.rs (FFI 131) — classical FOL with native equality; trust tier 2. - src/rust/provers/ileancop.rs (FFI 132) — intuitionistic FOL via prefix-checking; trust tier 3 (small kernel, sits in Level-4 cohort). - src/rust/provers/nanocop.rs (FFI 133) — minimal-kernel connection-method (~30 lines Prolog upstream); trust tier 3. Tableau-generator for arbitrary modal logics: - src/rust/provers/mettel2.rs (FFI 134) — JVM tableau-prover generator; defaults to S4 spec, configurable via mettel_logic metadata key; "Provable./Not provable./Unknown."; trust tier 2, complexity 4 (JVM cold-start + spec config). Wiring (mod.rs + ffi/mod.rs): - 7 new pub mod declarations (6 backends + connection_method). - 6 new ProverKind variants under three new sub-tiers (5e CAD, 5f connection-method, 5g tableau-generator). - Name parsers, all() listing, complexity / trust-tier / ml-score / binary-name tables, factory dispatch, FFI u8 forward+backward — all 6 backends fully wired via the 10-point checklist documented in PHASE-3-PROMPT.md. - Updated `test_kind_from_u8_out_of_range` boundary 127 → 134. ProverKind count: 129 → 135 (KeYmaera X landed earlier this session took 128 → 129; this commit takes 129 → 135 = 134+1 since enum elements are 0-indexed). Acceptance gate: - cargo build --lib: clean (0 errors, only pre-existing snake-case warnings on diagnostics fields). - cargo test --lib: **932 passed / 0 failed / 2 ignored** (up from 886 at session start). - qepcad: 8/8 pass - redlog: 8/8 pass - mettel2: 9/9 pass - mleancop: 3/3 pass - ileancop: 3/3 pass - nanocop: 3/3 pass - connection_method: 6/6 pass - 40 new tests across the 7 new files. - panic-attack: 0 unsafe_blocks / 0 panic_sites / 0 unwrap_calls across all 7 new files. Per-file LOC: qepcad 308, redlog 295, mettel2 297, mleancop 215, ileancop 215, nanocop 215, connection_method 110. Total ~1655 LOC, inside the brief's ~1500 LOC estimate (the extra ~150 are the shared helper module factored out per the brief's "duplication > 30 LOC → factor common helpers" guidance). Closes Phase 3 of the ECHIDNA expansion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c7e8a09 commit dec54bf

9 files changed

Lines changed: 1931 additions & 3 deletions

File tree

src/rust/ffi/mod.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,12 @@ pub fn kind_from_u8(kind: u8) -> Option<ProverKind> {
530530
126 => Some(ProverKind::Stainless),
531531
127 => Some(ProverKind::LiquidHaskell),
532532
128 => Some(ProverKind::KeYmaeraX),
533+
129 => Some(ProverKind::Qepcad),
534+
130 => Some(ProverKind::Redlog),
535+
131 => Some(ProverKind::MleanCoP),
536+
132 => Some(ProverKind::IleanCoP),
537+
133 => Some(ProverKind::NanoCoP),
538+
134 => Some(ProverKind::MetTeL2),
533539
_ => None,
534540
}
535541
}
@@ -1272,6 +1278,12 @@ pub fn kind_to_u8(kind: ProverKind) -> u8 {
12721278
ProverKind::Stainless => 126,
12731279
ProverKind::LiquidHaskell => 127,
12741280
ProverKind::KeYmaeraX => 128,
1281+
ProverKind::Qepcad => 129,
1282+
ProverKind::Redlog => 130,
1283+
ProverKind::MleanCoP => 131,
1284+
ProverKind::IleanCoP => 132,
1285+
ProverKind::NanoCoP => 133,
1286+
ProverKind::MetTeL2 => 134,
12751287
}
12761288
}
12771289

@@ -1456,9 +1468,12 @@ mod tests {
14561468

14571469
#[test]
14581470
fn test_kind_from_u8_out_of_range() {
1459-
// 0–127 are valid; 128+ are out of range.
1460-
// (Boundary moved to 127 on 2026-04-26 adding Phase 1a/1b ATPs and Phase 5 backends.)
1461-
assert!(kind_from_u8(128).is_none());
1471+
// 0–134 are valid; 135+ are out of range.
1472+
// (Boundary moved to 134 on 2026-04-27 adding Phase 3
1473+
// backends — KeYmaeraX (128), Qepcad (129), Redlog (130),
1474+
// MleanCoP (131), IleanCoP (132), NanoCoP (133), MetTeL2 (134).
1475+
// Previous boundary was 127 — Phase 1a/1b ATPs + Phase 5.)
1476+
assert!(kind_from_u8(135).is_none());
14621477
assert!(kind_from_u8(200).is_none());
14631478
assert!(kind_from_u8(255).is_none());
14641479
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// SPDX-FileCopyrightText: 2026 ECHIDNA Project Team
2+
// SPDX-License-Identifier: PMPL-1.0-or-later
3+
4+
//! Shared helpers for the connection-method prover family
5+
//! (MleanCoP / ileanCoP / nanoCoP).
6+
//!
7+
//! These three Prolog-based backends share TPTP-shaped input,
8+
//! SZS-marker output, and the same SWI-Prolog spawn pattern. This
9+
//! module factors out the common pieces so each backend file is a
10+
//! thin wrapper.
11+
12+
#![allow(dead_code)]
13+
14+
use anyhow::Result;
15+
use crate::core::ProofState;
16+
17+
/// Convert a `ProofState` into a TPTP fof-formatted input.
18+
///
19+
/// Same shape as `metitarski::to_tptp` but factored here so the
20+
/// connection-method trio share a single serialiser.
21+
pub(crate) fn to_tptp(state: &ProofState) -> String {
22+
let mut tptp = String::new();
23+
for (i, axiom) in state.context.axioms.iter().enumerate() {
24+
tptp.push_str(&format!("fof(axiom_{}, axiom, {}).\n", i, axiom));
25+
}
26+
for (i, def) in state.context.definitions.iter().enumerate() {
27+
tptp.push_str(&format!("fof(definition_{}, axiom, {}).\n", i, def));
28+
}
29+
if let Some(goal) = state.goals.first() {
30+
tptp.push_str(&format!("fof(conjecture, conjecture, {}).\n", goal.target));
31+
}
32+
tptp
33+
}
34+
35+
/// Parse a connection-method prover's stdout for SZS markers and the
36+
/// native "matrix proof found" signal that the leanCoP family emits.
37+
///
38+
/// Returns `Ok(true)` for theorem-status outputs, `Ok(false)` for
39+
/// counter-satisfiable / refuted, and `Err` for inconclusive
40+
/// (`GaveUp`, timeout, parse error).
41+
pub(crate) fn parse_szs_or_native(output: &str) -> Result<bool> {
42+
let lower = output.to_ascii_lowercase();
43+
44+
// SZS markers (TPTP convention).
45+
if lower.contains("szs status theorem") {
46+
return Ok(true);
47+
}
48+
if lower.contains("szs status countersatisfiable")
49+
|| lower.contains("szs status counter-satisfiable")
50+
|| lower.contains("szs status unsatisfiable")
51+
{
52+
return Ok(false);
53+
}
54+
if lower.contains("szs status gaveup") || lower.contains("szs status gave up") {
55+
return Err(anyhow::anyhow!(
56+
"Connection-method prover gave up: {}",
57+
output.lines().take(10).collect::<Vec<_>>().join("\n")
58+
));
59+
}
60+
61+
// Native leanCoP-family signals.
62+
if lower.contains("matrix proof found") || lower.contains("proof found") {
63+
return Ok(true);
64+
}
65+
if lower.contains("no proof") || lower.contains("not a theorem") {
66+
return Ok(false);
67+
}
68+
if lower.contains("error") || lower.contains("exception") {
69+
return Err(anyhow::anyhow!(
70+
"Connection-method prover error: {}",
71+
output.lines().take(10).collect::<Vec<_>>().join("\n")
72+
));
73+
}
74+
75+
Err(anyhow::anyhow!(
76+
"Connection-method prover output inconclusive: {}",
77+
output.lines().take(10).collect::<Vec<_>>().join("\n")
78+
))
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
use crate::core::{Goal, Term};
85+
86+
#[test]
87+
fn test_to_tptp_emits_axioms_and_conjecture() {
88+
let mut state = ProofState::default();
89+
state.context.axioms.push("p | ~ p".to_string());
90+
state.goals.push(Goal {
91+
id: "g0".to_string(),
92+
target: Term::Const("p".to_string()),
93+
hypotheses: vec![],
94+
});
95+
let tptp = to_tptp(&state);
96+
assert!(tptp.contains("fof(axiom_0, axiom, p | ~ p)."));
97+
assert!(tptp.contains("fof(conjecture, conjecture, p)."));
98+
}
99+
100+
#[test]
101+
fn test_parse_szs_theorem() {
102+
let r = parse_szs_or_native("% SZS status Theorem\n").expect("expected Ok");
103+
assert!(r);
104+
}
105+
106+
#[test]
107+
fn test_parse_szs_countersatisfiable() {
108+
let r = parse_szs_or_native("% SZS status CounterSatisfiable\n").expect("expected Ok");
109+
assert!(!r);
110+
}
111+
112+
#[test]
113+
fn test_parse_szs_gaveup_is_err() {
114+
assert!(parse_szs_or_native("% SZS status GaveUp\n").is_err());
115+
}
116+
117+
#[test]
118+
fn test_parse_native_proof_found() {
119+
let r = parse_szs_or_native("Matrix proof found").expect("expected Ok");
120+
assert!(r);
121+
}
122+
123+
#[test]
124+
fn test_parse_native_no_proof() {
125+
let r = parse_szs_or_native("No proof.\n").expect("expected Ok");
126+
assert!(!r);
127+
}
128+
}

src/rust/provers/ileancop.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// SPDX-FileCopyrightText: 2026 ECHIDNA Project Team
2+
// SPDX-License-Identifier: PMPL-1.0-or-later
3+
4+
//! ileanCoP backend — connection-method prover for intuitionistic
5+
//! first-order logic.
6+
//!
7+
//! ileanCoP is the intuitionistic-logic member of the leanCoP family.
8+
//! It uses the *intuitionistic* connection method (with
9+
//! prefix-checking to enforce constructive validity) rather than the
10+
//! classical version used by MleanCoP.
11+
//!
12+
//! ## Why this backend exists
13+
//!
14+
//! Constructive provers (Coq, Agda, Lean, Idris2) live in
15+
//! intuitionistic logic. ileanCoP gives ECHIDNA a *fully automated*
16+
//! intuitionistic ATP that complements those interactive provers —
17+
//! useful for offloading the trivial-but-tedious lemmas that arise
18+
//! during constructive proof development.
19+
//!
20+
//! ## Input format
21+
//!
22+
//! TPTP fof, same as MleanCoP. The "intuitionistic" interpretation
23+
//! is enforced by the prover — the input syntax is unchanged.
24+
//!
25+
//! ## CLI invocation
26+
//!
27+
//! `swipl -g "ileancop:prove(...)"` or via stdin like MleanCoP.
28+
//!
29+
//! ## Output parsing
30+
//!
31+
//! Same SZS / native markers as MleanCoP — see
32+
//! `connection_method::parse_szs_or_native`.
33+
//!
34+
//! ## Integration tier
35+
//!
36+
//! Tier-5d / Phase-3. Trust tier 3 (intuitionistic kernel is *small*
37+
//! and well-studied — connection-method gives a checkable matrix
38+
//! proof object with a 30-line core). Complexity 3.
39+
40+
#![allow(dead_code)]
41+
42+
use anyhow::{Context, Result};
43+
use async_trait::async_trait;
44+
use std::path::PathBuf;
45+
use std::process::Stdio;
46+
use tokio::io::AsyncWriteExt;
47+
use tokio::process::Command;
48+
49+
use super::connection_method;
50+
use super::{ProverBackend, ProverConfig, ProverKind};
51+
use crate::core::{Goal, ProofState, Tactic, TacticResult, Term};
52+
53+
/// ileanCoP backend.
54+
pub struct IleanCopBackend {
55+
config: ProverConfig,
56+
}
57+
58+
impl IleanCopBackend {
59+
pub fn new(config: ProverConfig) -> Self {
60+
IleanCopBackend { config }
61+
}
62+
}
63+
64+
#[async_trait]
65+
impl ProverBackend for IleanCopBackend {
66+
fn kind(&self) -> ProverKind {
67+
ProverKind::IleanCoP
68+
}
69+
70+
async fn version(&self) -> Result<String> {
71+
let output = Command::new(&self.config.executable)
72+
.arg("--version")
73+
.output()
74+
.await
75+
.context("Failed to run ileancop --version")?;
76+
let stdout = String::from_utf8_lossy(&output.stdout);
77+
Ok(stdout.lines().next().unwrap_or("ileancop").trim().to_string())
78+
}
79+
80+
async fn parse_file(&self, path: PathBuf) -> Result<ProofState> {
81+
let content = super::bounded_read_proof_file(&path)
82+
.await
83+
.context("Failed to read TPTP input")?;
84+
let mut state = self.parse_string(&content).await?;
85+
state.metadata.insert(
86+
"source_path".to_string(),
87+
serde_json::Value::String(path.to_string_lossy().into_owned()),
88+
);
89+
Ok(state)
90+
}
91+
92+
async fn parse_string(&self, content: &str) -> Result<ProofState> {
93+
let mut state = ProofState::default();
94+
state.metadata.insert(
95+
"tptp_source".to_string(),
96+
serde_json::Value::String(content.to_string()),
97+
);
98+
for line in content.lines() {
99+
let line = line.trim();
100+
if line.is_empty() || line.starts_with('%') {
101+
continue;
102+
}
103+
if line.starts_with("fof(") {
104+
if let Some(formula) = line.split(',').nth(2) {
105+
let formula = formula.trim_end_matches(").").trim();
106+
if line.contains(", axiom,") {
107+
state.context.axioms.push(formula.to_string());
108+
} else if line.contains(", conjecture,") {
109+
state.goals.push(Goal {
110+
id: format!("goal_{}", state.goals.len()),
111+
target: Term::Const(formula.to_string()),
112+
hypotheses: vec![],
113+
});
114+
}
115+
}
116+
}
117+
}
118+
Ok(state)
119+
}
120+
121+
async fn apply_tactic(&self, _state: &ProofState, _tactic: &Tactic) -> Result<TacticResult> {
122+
Err(anyhow::anyhow!(
123+
"ileanCoP is fully automated — interactive tactics not supported"
124+
))
125+
}
126+
127+
async fn verify_proof(&self, state: &ProofState) -> Result<bool> {
128+
let tptp = connection_method::to_tptp(state);
129+
let mut child = Command::new(&self.config.executable)
130+
.stdin(Stdio::piped())
131+
.stdout(Stdio::piped())
132+
.stderr(Stdio::piped())
133+
.spawn()
134+
.context("Failed to spawn ileancop")?;
135+
{
136+
let stdin = child
137+
.stdin
138+
.as_mut()
139+
.ok_or_else(|| anyhow::anyhow!("Failed to open ileancop stdin"))?;
140+
stdin
141+
.write_all(tptp.as_bytes())
142+
.await
143+
.context("Failed to write to ileancop stdin")?;
144+
}
145+
let output = child
146+
.wait_with_output()
147+
.await
148+
.context("Failed to wait for ileancop")?;
149+
let combined = format!(
150+
"{}\n{}",
151+
String::from_utf8_lossy(&output.stdout),
152+
String::from_utf8_lossy(&output.stderr)
153+
);
154+
connection_method::parse_szs_or_native(&combined)
155+
}
156+
157+
async fn export(&self, state: &ProofState) -> Result<String> {
158+
Ok(connection_method::to_tptp(state))
159+
}
160+
161+
async fn suggest_tactics(
162+
&self,
163+
_state: &ProofState,
164+
_limit: usize,
165+
) -> Result<Vec<Tactic>> {
166+
Ok(vec![])
167+
}
168+
169+
async fn search_theorems(&self, _pattern: &str) -> Result<Vec<String>> {
170+
Ok(vec![])
171+
}
172+
173+
fn config(&self) -> &ProverConfig {
174+
&self.config
175+
}
176+
177+
fn set_config(&mut self, config: ProverConfig) {
178+
self.config = config;
179+
}
180+
}
181+
182+
#[cfg(test)]
183+
mod tests {
184+
use super::*;
185+
186+
#[test]
187+
fn test_ileancop_kind() {
188+
let config = ProverConfig::default();
189+
let backend = IleanCopBackend::new(config);
190+
assert_eq!(backend.kind(), ProverKind::IleanCoP);
191+
}
192+
193+
#[tokio::test]
194+
async fn test_ileancop_parse_string_extracts_axioms_and_goal() {
195+
let config = ProverConfig::default();
196+
let backend = IleanCopBackend::new(config);
197+
let tptp = "fof(a1, axiom, p => p).\nfof(conj, conjecture, p => p).\n";
198+
let state = backend.parse_string(tptp).await.expect("parse_string failed");
199+
assert_eq!(state.context.axioms.len(), 1);
200+
assert_eq!(state.goals.len(), 1);
201+
}
202+
203+
#[test]
204+
fn test_ileancop_export_emits_conjecture() {
205+
let config = ProverConfig::default();
206+
let _backend = IleanCopBackend::new(config);
207+
let mut state = ProofState::default();
208+
state.goals.push(Goal {
209+
id: "g0".to_string(),
210+
target: Term::Const("p => p".to_string()),
211+
hypotheses: vec![],
212+
});
213+
let tptp = connection_method::to_tptp(&state);
214+
assert!(tptp.contains("fof(conjecture, conjecture, p => p)."));
215+
}
216+
}

0 commit comments

Comments
 (0)