Skip to content

Commit 3fb48ed

Browse files
ArghDA M9 follow-on: Isabelle session-qualified imports (M9 → 98%) (#52)
## What A theory that imports from another session — `imports "HOL-Library.FuncSet"` — is now checkable. This is the **last** of the heavy-tail completion cuts. Ground-truthed first (the important step): **flat sibling imports already worked** — the adapter stages every sibling `.thy` into the session, so `imports Main Foo` on a sibling `Foo` already resolved. So the real gap was narrower than the milestone text suggested: only *session-qualified* imports. ## How The gap: the generated ROOT said `= HOL +` with no route to `HOL-Library`, so the build errored "Cannot load theory". Fix: `session_prefixes` scans all staged theories' `imports` clauses for qualified `"Session.Theory"` forms (session = the segment before the first `.`; Isabelle session names are dot-free) and emits a `sessions "<S>" …` clause in the generated ROOT, so `isabelle build` pulls those sessions' heaps. The distribution session heaps ship prebuilt, so the build stays fast (~15 s). ## Verification (actually run) - `cargo fmt --check` / `clippy -D warnings` — clean - `cargo test` — **176 pass** (+1 `session_prefixes` unit) - `check-spdx.sh` — OK **Dogfooded vs real Isabelle2025:** | case | result | |---|---| | `Q.thy` importing `"HOL-Library.FuncSet"`, using its Pi-notation | `proven` (was `error` before) | | flat-sibling regression (`Bar` imports sibling `Foo`) | still `proven` | ## Scope M9 → 98%. Residual: a project with its OWN multi-session ROOT (a locally-defined session importing another local session's theories) — niche; single-theory, flat-sibling and distribution-session-qualified imports all work. **This completes the heavy-tail completion cuts** (Coq dependency-ordering #50, Mizar cross-refs #51, Isabelle session-qualified imports). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012MpYSh6Wy8YMBH2E3qVyT7 --- _Generated by [Claude Code](https://claude.ai/code/session_012MpYSh6Wy8YMBH2E3qVyT7)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent da34183 commit 3fb48ed

2 files changed

Lines changed: 89 additions & 12 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ milestones = [
4949
{ name = "M6: Lean4 adapter (lake build + #print axioms audit)", completion = 100 },
5050
{ name = "M7: Echidna dispatch seam (optional route to orchestrator :8090, same Outcome)", completion = 70 },
5151
{ name = "M8: Coq/Rocq adapter (--heavy provisioning; Section-aware postulate classifier)", completion = 97 },
52-
{ name = "M9: Isabelle adapter (--heavy; dogfood tropical-resource-typing .thy)", completion = 90 },
52+
{ name = "M9: Isabelle adapter (--heavy; dogfood tropical-resource-typing .thy)", completion = 98 },
5353
{ name = "M10: Mizar adapter (detect-only first; highest uncertainty)", completion = 95 },
5454
{ name = "M11: studio JSON freeze (reason/0.1) + Groove manifest", completion = 100 },
5555
]
@@ -114,6 +114,25 @@ milestones = [
114114
# --backend idris2` over an ipkg fixture → crt_roots ['App'], App+Helper wired.
115115
# M4 now 95% (remaining: totality-hole `?name` + per-def `partial` lint).
116116
#
117+
# M9 FOLLOW-ON landed 2026-07-01 (M9 90% → 98%): Isabelle session-qualified
118+
# imports. GROUND-TRUTHED FIRST (the important step): FLAT sibling imports were
119+
# ALREADY working — the adapter copies every sibling .thy into the generated
120+
# session, so `imports Main Foo` (Foo a sibling) already resolved (dogfooded:
121+
# Bar imports Foo → proven). The genuine gap was SESSION-QUALIFIED imports like
122+
# `imports "HOL-Library.FuncSet"` (a theory from another SESSION): the generated
123+
# ROOT said `= HOL +` with no way to reach HOL-Library → "Cannot load theory"
124+
# error. Fix: `session_prefixes` scans all staged theories' imports clauses for
125+
# qualified `"Session.Theory"` forms (session = segment before the first `.`;
126+
# Isabelle session names are dot-free) and emits a `sessions "<S>" …` clause in
127+
# the ROOT, so `isabelle build` pulls those sessions' heaps. Ground-truthed the
128+
# HOL-Library heap ships prebuilt (dry-run clean), so the build is ~15 s.
129+
# 176 tests (+1 session_prefixes unit). DOGFOODED vs real Isabelle2025: Q.thy
130+
# `imports "HOL-Library.FuncSet"` using Pi-notation → proven (was error pre-fix);
131+
# flat-sibling regression (Bar imports Foo) → still proven. Residual 2%: a
132+
# project with its OWN multi-session ROOT (a locally-defined session importing
133+
# another local session's theories) — niche; single-theory + flat-sibling +
134+
# distribution-session-qualified all work.
135+
#
117136
# M10 FOLLOW-ON landed 2026-07-01 (M10 85% → 95%): Mizar local-article cross-
118137
# references. Was: `theorems X;` on a sibling article errored (X not in the
119138
# library). GROUND-TRUTHED the export pipeline vs real Mizar 8.1.15: to Require
@@ -473,7 +492,7 @@ actions = [
473492
"M9 (Isabelle): DONE (90%) — Isabelle2025 provisioned + adapter built + dogfooded (proven/admitted/postulated/error) via session-build; ROOT-based roots, imports-clause edges. Remaining: session-qualified imports + dependency-ordered multi-session builds.",
474493
"M10 (Mizar): DONE (85%) — turned out to be a REAL verify, not detect-only: static i386 verifier runs on x86_64; provisioned + adapter (accom+verifier+.err) + dogfooded (proven/error). HEAVY TAIL M8-M10 COMPLETE. Remaining: local-article prel export.",
475494
"M7 client INVESTIGATED 2026-07-01 → BLOCKED (stub stays): no confirmed Echidna HTTP contract exists (dispatch.rs says API unconfirmed; ECOSYSTEM.a2ml gives no endpoints/schema; the echidna repo is out of this session's access scope). Building a client would mean inventing an unverifiable contract — forbidden. Unblock: a published Echidna API + a reachable instance to dogfood; then wire Dispatch::run behind a cargo feature. ROADMAP TERMINUS — all tractable items done; M7's client is the sole residual, externally blocked.",
476-
"HEAVY-TAIL COMPLETION CUTS (owner asked 2026-07-01, foundation-first, one PR each, real-tool dogfood): [DONE] Coq dependency-ordered compilation (M8 90→97%); [DONE] Mizar local-article cross-refs via accom/verifier/exporter/transfer export pipeline (M10 85→95%, theorems path dogfooded); [NEXT/LAST] Isabelle session-qualified imports + multi-session builds (M9 90→~100%). Residual after: Coq _CoqProject custom -Q/-R prefixes (3%); Mizar definitions/notations cross-refs not separately dogfooded (5%); arghda-studio visual layer (separate repo, out of this session's scope; AffineScript).",
495+
"HEAVY-TAIL COMPLETION CUTS COMPLETE (owner asked 2026-07-01, foundation-first, one PR each, real-tool dogfood): [DONE] Coq dependency-ordered compilation (M8 90→97%); [DONE] Mizar local-article cross-refs via export pipeline (M10 85→95%); [DONE] Isabelle session-qualified imports (M9 90→98%). All three heavy provers now handle multi-file/cross-session projects, dogfooded against their real tools. Small residuals (owner's call, none blocking): Coq _CoqProject custom -Q/-R prefixes (3%); Mizar definitions/notations cross-refs not separately dogfooded (5%); Isabelle own-multi-session-ROOT projects (2%). Out of this session's scope: arghda-studio visual layer (separate repo, AffineScript).",
477496
]
478497

479498
[maintenance-status]
@@ -482,7 +501,9 @@ last-result = "pass" # unknown | pass | warn | fail
482501
open-warnings = 0
483502
open-failures = 0
484503
# `just check` (fmt-check + clippy -D warnings + build + test + SPDX) green
485-
# post-M10-crossref: 132 lib + 43 integration = 175 tests pass. M10 Mizar
504+
# post-M9-sessionqual: 133 lib + 43 integration = 176 tests pass. M9 Isabelle
505+
# session-qualified imports dogfooded vs real Isabelle2025 (imports
506+
# "HOL-Library.FuncSet" → proven; flat sibling still proven). M10 Mizar
486507
# local-article cross-refs dogfooded vs real Mizar 8.1.15 (y requires local x →
487508
# proven; broken dep → dependent error). M8 dependency-ordered Coq compilation
488509
# dogfooded vs real coqc 8.18.0 (A←B←C chain → all proven; transitive admit

src/prover/isabelle.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
//! Theory names map to files like every other assistant (`Foo` ↔ `Foo.thy`),
2424
//! so [`crate::graph::module_name_of`] is reused (a flat tree's dotted name is
2525
//! just the theory stem). Import edges come from the header `imports` clause
26-
//! (`theory Foo imports Bar Baz begin`). Roots are the theories a `ROOT` file
27-
//! lists under a `theories` section — the genuine Isabelle entry-point
28-
//! convention (analogue of Agda's `All.agda`). Session-qualified / relative-
29-
//! path imports and dependency-ordered multi-session builds are documented
30-
//! follow-ons.
26+
//! (`theory Foo imports Bar Baz begin`). Flat in-tree imports resolve because
27+
//! every sibling `.thy` is staged into the session; **session-qualified**
28+
//! imports (`imports "HOL-Library.FuncSet"`) resolve too — [`session_prefixes`]
29+
//! collects each qualified import's session (`HOL-Library`) across the staged
30+
//! theories and emits a `sessions` clause in the generated ROOT, so
31+
//! `isabelle build` pulls those sessions' heaps. Roots are the theories a
32+
//! `ROOT` file lists under a `theories` section — the genuine Isabelle
33+
//! entry-point convention (analogue of Agda's `All.agda`).
3134
3235
use super::{probe_tool_arg, Backend, BackendKind, Outcome, Probe, Verdict};
3336
use crate::graph;
@@ -94,8 +97,11 @@ impl Backend for Isabelle {
9497
return Ok(Outcome::unavailable(BackendKind::Assistant));
9598
}
9699

97-
// Copy every sibling `.thy` (basename) so in-tree imports resolve,
98-
// then ensure the target is present (it may sit outside include_root).
100+
// Copy every sibling `.thy` (basename) so flat in-tree imports resolve,
101+
// and collect any session-qualified import prefixes (`"HOL-Library.X"`
102+
// → session `HOL-Library`) across all staged theories so they can be
103+
// declared in the ROOT's `sessions` clause.
104+
let mut sessions: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
99105
for entry in WalkDir::new(include_root)
100106
.into_iter()
101107
.filter_map(|e| e.ok())
@@ -105,11 +111,26 @@ impl Backend for Isabelle {
105111
if let Some(name) = p.file_name() {
106112
let _ = fs::copy(p, session_dir.join(name));
107113
}
114+
if let Ok(s) = fs::read_to_string(p) {
115+
sessions.extend(session_prefixes(&s));
116+
}
108117
}
109118
}
110119
let _ = fs::copy(file, session_dir.join(format!("{stem}.thy")));
111-
112-
let root = format!("session \"ArghdaCheck\" = \"HOL\" +\n theories\n {stem}\n");
120+
let src_for_sessions = fs::read_to_string(file).unwrap_or_default();
121+
sessions.extend(session_prefixes(&src_for_sessions));
122+
123+
// Generate the ROOT: base HOL, a `sessions` clause for every qualified
124+
// import prefix (so `isabelle build` can pull those sessions), then the
125+
// target theory.
126+
let mut root = String::from("session \"ArghdaCheck\" = \"HOL\" +\n");
127+
if !sessions.is_empty() {
128+
root.push_str(" sessions\n");
129+
for s in &sessions {
130+
root.push_str(&format!(" \"{s}\"\n"));
131+
}
132+
}
133+
root.push_str(&format!(" theories\n {stem}\n"));
113134
if fs::write(session_dir.join("ROOT"), root).is_err() {
114135
let _ = fs::remove_dir_all(&session_dir);
115136
return Ok(Outcome::unavailable(BackendKind::Assistant));
@@ -241,6 +262,26 @@ fn parse_imports(contents: &str) -> Vec<String> {
241262
out
242263
}
243264

265+
/// The session qualifiers of a theory's `imports` clause: for each
266+
/// session-qualified import `"Session.Theory"` (a dotted name — Isabelle
267+
/// session names are themselves dot-free, so the session is the segment before
268+
/// the first `.`), yield `Session`. Bare theory names (`Main`, a flat sibling)
269+
/// have no dot and contribute nothing. These become the ROOT's `sessions`
270+
/// clause so `isabelle build` can pull the imported session's heap.
271+
fn session_prefixes(contents: &str) -> Vec<String> {
272+
parse_imports(contents)
273+
.into_iter()
274+
.filter_map(|imp| {
275+
let (pre, rest) = imp.split_once('.')?;
276+
if pre.is_empty() || rest.is_empty() {
277+
None
278+
} else {
279+
Some(pre.to_string())
280+
}
281+
})
282+
.collect()
283+
}
284+
244285
/// Normalise an `imports`/`ROOT` theory token to a bare theory name: strip
245286
/// quotes, then take the final path segment (`"../Other/Baz"` → `Baz`).
246287
fn theory_token(tok: &str) -> String {
@@ -353,6 +394,21 @@ mod tests {
353394
assert!(!imports.iter().any(|i| i == "x"));
354395
}
355396

397+
#[test]
398+
fn session_prefixes_only_from_qualified_imports() {
399+
// A session-qualified import contributes its session; bare theory names
400+
// (Main, a flat sibling) do not.
401+
let thy = "theory Q\n imports Main Foo \"HOL-Library.FuncSet\" \"HOL-Analysis.Derivative\"\nbegin\nend\n";
402+
let mut ps = session_prefixes(thy);
403+
ps.sort();
404+
assert_eq!(
405+
ps,
406+
vec!["HOL-Analysis".to_string(), "HOL-Library".to_string()]
407+
);
408+
// No qualified imports ⇒ no sessions.
409+
assert!(session_prefixes("theory T imports Main Bar begin end\n").is_empty());
410+
}
411+
356412
#[test]
357413
fn verdict_is_honest_about_sorry_axiom_and_clean() {
358414
assert_eq!(

0 commit comments

Comments
 (0)