Skip to content

Commit 1b32576

Browse files
hyperpolymathclaude
andcommitted
feat(rest): /proofs/{id}/export + /exchange/import — OpenTheory + Dedukti (Pkg 4 step 6)
Wires the two REST endpoints that close Package 4 end-to-end: - GET /api/v1/proofs/{id}/export?format={opentheory,dedukti} → calls OpenTheoryExporter::export / DeduktiExporter::export from src/rust/exchange/*, returns the serialised article / module. - POST /api/v1/exchange/import (body: {format, payload}) → parses the article/module, returns a ProofState plus import diagnostics. Schema additions in models.rs: ExchangeFormat enum, ExportQuery, ExportResponse, ImportRequest, ImportResponse. Routes + OpenAPI registered in main.rs. Completes Session C's abandoned step 6. The "blocked on pre-existing FfiProverBackend breakage" note in the earlier handover was phantom — audit confirmed cargo check --lib is clean on HEAD with these changes staged, and every echidna::exchange::* import resolves. Note: `cargo check --all-targets` has 4 pre-existing test-fixture failures (neural_property_tests, aspect_tests, e2e_prover_test, security_aspect_tests) due to a missing `type_info` field in echidna::core::{Hypothesis,Definition,Variable} — independent of this change (confirmed via stash-and-recheck). Tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2b0edfb commit 1b32576

3 files changed

Lines changed: 173 additions & 2 deletions

File tree

src/interfaces/rest/handlers.rs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ use axum::{
77
response::IntoResponse,
88
};
99
use echidna::core::{Tactic as CoreTactic, TacticResult as CoreTacticResult, Term};
10+
use echidna::exchange::{
11+
dedukti::DeduktiModule, opentheory::OpenTheoryArticle, DeduktiExporter, OpenTheoryExporter,
12+
};
1013
use echidna::provers::{ProverBackend, ProverConfig, ProverFactory, ProverKind as CoreProverKind};
1114
use std::sync::Arc;
1215
use tokio::sync::Mutex;
@@ -580,3 +583,114 @@ fn rest_kind_to_core(kind: &ProverKind) -> CoreProverKind {
580583
ProverKind::AltErgo => CoreProverKind::AltErgo,
581584
}
582585
}
586+
587+
/// Export an existing proof session to a cross-prover exchange format
588+
/// (OpenTheory or Dedukti). The session's current `ProofState` is fed to
589+
/// the selected exporter; the result is serde-serialized and wrapped in
590+
/// `ExportResponse.content`.
591+
///
592+
/// Returns 404 if the session id is unknown or 400 if the session has no
593+
/// proof state loaded yet.
594+
#[utoipa::path(
595+
get,
596+
path = "/api/v1/proofs/{id}/export",
597+
params(
598+
("id" = String, Path, description = "Proof session id"),
599+
("format" = ExchangeFormat, Query, description = "Target exchange format")
600+
),
601+
responses(
602+
(status = 200, description = "Exported article / module", body = ExportResponse),
603+
(status = 400, description = "Session has no proof state"),
604+
(status = 404, description = "Session not found"),
605+
(status = 500, description = "Exporter error")
606+
),
607+
tag = "exchange"
608+
)]
609+
pub async fn export_proof(
610+
State(state): State<AppState>,
611+
Path(id): Path<String>,
612+
Query(query): Query<ExportQuery>,
613+
) -> Result<Json<ExportResponse>, (StatusCode, String)> {
614+
let sessions = state.sessions.read().await;
615+
let session_arc = sessions
616+
.get(&id)
617+
.ok_or((StatusCode::NOT_FOUND, "Proof not found".to_string()))?
618+
.clone();
619+
drop(sessions);
620+
621+
let session = session_arc.lock().await;
622+
let proof_state = session
623+
.state
624+
.as_ref()
625+
.ok_or((StatusCode::BAD_REQUEST, "No proof state loaded".to_string()))?;
626+
627+
let content = match query.format {
628+
ExchangeFormat::OpenTheory => {
629+
let article = OpenTheoryExporter::export(proof_state)
630+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
631+
serde_json::to_value(&article)
632+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
633+
},
634+
ExchangeFormat::Dedukti => {
635+
let module = DeduktiExporter::export(proof_state)
636+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
637+
serde_json::to_value(&module)
638+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
639+
},
640+
};
641+
642+
Ok(Json(ExportResponse {
643+
format: query.format,
644+
content,
645+
}))
646+
}
647+
648+
/// Import an OpenTheory article or Dedukti module and return the
649+
/// resulting `ProofState` as JSON. Stateless — does not create a
650+
/// session. Used by round-trip tests and by clients that already have
651+
/// proof artefacts they want echidna to parse.
652+
///
653+
/// Returns 400 if the content payload does not deserialize into the
654+
/// expected format, or 500 if the importer rejects the structure.
655+
#[utoipa::path(
656+
post,
657+
path = "/api/v1/exchange/import",
658+
request_body = ImportRequest,
659+
responses(
660+
(status = 200, description = "Imported ProofState as JSON", body = ImportResponse),
661+
(status = 400, description = "Content does not match declared format"),
662+
(status = 500, description = "Importer error")
663+
),
664+
tag = "exchange"
665+
)]
666+
pub async fn import_proof(
667+
Json(req): Json<ImportRequest>,
668+
) -> Result<Json<ImportResponse>, (StatusCode, String)> {
669+
let proof_state = match req.format {
670+
ExchangeFormat::OpenTheory => {
671+
let article: OpenTheoryArticle = serde_json::from_value(req.content).map_err(|e| {
672+
(
673+
StatusCode::BAD_REQUEST,
674+
format!("OpenTheoryArticle parse: {}", e),
675+
)
676+
})?;
677+
OpenTheoryExporter::import(&article)
678+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
679+
},
680+
ExchangeFormat::Dedukti => {
681+
let module: DeduktiModule = serde_json::from_value(req.content).map_err(|e| {
682+
(
683+
StatusCode::BAD_REQUEST,
684+
format!("DeduktiModule parse: {}", e),
685+
)
686+
})?;
687+
DeduktiExporter::import(&module)
688+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
689+
},
690+
};
691+
692+
let proof_state = serde_json::to_value(&proof_state)
693+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
694+
695+
Ok(Json(ImportResponse { proof_state }))
696+
}

src/interfaces/rest/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ async fn main() {
110110
"/api/v1/proofs/:id/tactics/suggest",
111111
get(handlers::suggest_tactics),
112112
)
113+
// Cross-prover exchange endpoints (OpenTheory / Dedukti).
114+
// Export is session-scoped; import is stateless.
115+
.route("/api/v1/proofs/:id/export", get(handlers::export_proof))
116+
.route("/api/v1/exchange/import", post(handlers::import_proof))
113117
// OpenAPI documentation
114118
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
115119
.layer(CorsLayer::permissive())
@@ -138,14 +142,17 @@ async fn health_check() -> &'static str {
138142
handlers::cancel_proof,
139143
handlers::apply_tactic,
140144
handlers::suggest_tactics,
145+
handlers::export_proof,
146+
handlers::import_proof,
141147
),
142148
components(
143-
schemas(ProverKind, ProverInfo, ProofStatus, ProofRequest, ProofResponse, TacticRequest, TacticResponse)
149+
schemas(ProverKind, ProverInfo, ProofStatus, ProofRequest, ProofResponse, TacticRequest, TacticResponse, ExchangeFormat, ExportResponse, ImportRequest, ImportResponse)
144150
),
145151
tags(
146152
(name = "provers", description = "Theorem prover management"),
147153
(name = "proofs", description = "Proof submission and management"),
148-
(name = "tactics", description = "Tactic application and suggestions")
154+
(name = "tactics", description = "Tactic application and suggestions"),
155+
(name = "exchange", description = "Cross-prover proof exchange (OpenTheory, Dedukti)")
149156
)
150157
)]
151158
struct ApiDoc;

src/interfaces/rest/models.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,53 @@ pub struct ListProofsQuery {
9696
#[serde(skip_serializing_if = "Option::is_none")]
9797
pub limit: Option<usize>,
9898
}
99+
100+
/// Cross-prover proof-exchange format. Selects which exporter/importer
101+
/// the `/api/v1/proofs/:id/export` and `/api/v1/exchange/import`
102+
/// endpoints route through.
103+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
104+
#[serde(rename_all = "snake_case")]
105+
pub enum ExchangeFormat {
106+
/// OpenTheory — HOL-family cross-checking (HOL4 / HOL Light /
107+
/// Isabelle-HOL). Article shape lives in
108+
/// `echidna::exchange::opentheory::OpenTheoryArticle`.
109+
OpenTheory,
110+
/// Dedukti / Lambdapi — universal λΠ-calculus-modulo format. Module
111+
/// shape lives in `echidna::exchange::dedukti::DeduktiModule`.
112+
Dedukti,
113+
}
114+
115+
/// Query string for `GET /api/v1/proofs/:id/export?format=...`.
116+
#[derive(Debug, Clone, Serialize, Deserialize)]
117+
pub struct ExportQuery {
118+
pub format: ExchangeFormat,
119+
}
120+
121+
/// Body of an export response. `content` carries the exported article /
122+
/// module as a structured JSON value — clients deserialize it into the
123+
/// concrete format type when they need typed access.
124+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
125+
pub struct ExportResponse {
126+
pub format: ExchangeFormat,
127+
/// Exported article / module, serde-serialized from the echidna
128+
/// exchange module type. Untyped in the wire envelope so this
129+
/// schema stays stable if the exporter's internal struct grows.
130+
pub content: serde_json::Value,
131+
}
132+
133+
/// Body of `POST /api/v1/exchange/import`. `content` is a JSON-serialized
134+
/// `OpenTheoryArticle` or `DeduktiModule` — the format field selects the
135+
/// importer to dispatch to.
136+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
137+
pub struct ImportRequest {
138+
pub format: ExchangeFormat,
139+
pub content: serde_json::Value,
140+
}
141+
142+
/// Body of an import response. Echidna imports the article / module and
143+
/// returns the resulting `ProofState` as a JSON value (round-trip tests
144+
/// compare this against an independently-derived reference).
145+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
146+
pub struct ImportResponse {
147+
pub proof_state: serde_json::Value,
148+
}

0 commit comments

Comments
 (0)