AtpClient.Isabelle (AtpClient v0.7.2)

Copy Markdown View Source

Client for Isabelle servers, built on top of IsabelleClient from the :isabelle_elixir package.

The Isabelle server cannot accept theory text over the wire — theories have to live as .thy files on a filesystem that both the BEAM node running AtpClient and the Isabelle server can see. This module handles the file bookkeeping for you: you pass the theory body as a string, and it is written into a configured shared directory before use_theories is invoked.

Configuration

config :atp_client, :isabelle,
  host: "isabelle.example.org",
  port: 9999,
  password: System.get_env("ISABELLE_PASSWORD"),
  session: "HOL"

When the BEAM and the Isabelle server run on the same host (the usual dev setup) no further configuration is required — :local_dir defaults to a subdirectory of System.tmp_dir!/0 and the library writes .thy files there before asking Isabelle to load them. Pass theory text in, get results back; the file bookkeeping is hidden.

Zero-config: isabelle on $PATH

If the isabelle executable is discoverable (either on $PATH or via the ISABELLE_TOOL environment variable), the bundled :isabelle_elixir package can spin up a local resident server on demand. This removes any need to configure host/port/password by hand:

# Start a throwaway server (uses `isabelle` from $PATH).
{:ok, server} = IsabelleClient.start_server()

opts = [host: server.host, port: server.port, password: server.password]

{:ok, session} = AtpClient.Isabelle.open_session(opts)
{:ok, %AtpClient.Result{status: :theorem}} =
  AtpClient.Isabelle.prove_theory(session, ~S"""
  lemma "P \<or> \<not> P" by auto
  """, "Example")
:ok = AtpClient.Isabelle.close_session(session)

# When you're done, stop the server:
IsabelleClient.Server.kill(server.name)

A minimal config/config.exs reduces the per-call opts to nothing — after the one-time start_server/0 call, put the returned coordinates into the application env once and forget about them.

Paths: local_dir vs. isabelle_dir

Only relevant when the BEAM node and the Isabelle server see the shared directory under different paths. In that case set both explicitly:

config :atp_client, :isabelle,
  local_dir: "/shared/problems",
  isabelle_dir: "/data/problems"

The two common cases:

  • Containers: BEAM writes to /shared/problems inside its container; the Isabelle server container has the same volume mounted at /data/problems.
  • Windows + Cygwin: BEAM on Windows sees C:/Users/you/thy; Isabelle started from a Cygwin shell sees the same directory as /cygdrive/c/Users/you/thy.

Paths given via isabelle_dir are passed through verbatim — the library does not try to translate Windows paths to POSIX or vice-versa. When unset, :isabelle_dir defaults to :local_dir.

Results

Every proving call returns {:ok, %AtpClient.Result{}} or {:error, reason}. Isabelle prints no SZS lines; the status is read from the server's messages by AtpClient.Isabelle.Verdict, and the result's evidence is {:backend, AtpClient.Isabelle}. output is the text of those messages, and details carries what the call knows beyond the status:

  • prove_theory/4 — %{payload: map()}, the use_theories payload as the server returned it.
  • prove_lemmas/4, query_lemmas/3, prove_tptp/3 and query/2 — %{lemmas: [lemma_result], payload: map()}, one entry per lemma in the order of the lemma declarations, each %{name: name, status: status}. The result's own status is the weakest link: :theorem only when every lemma was discharged, otherwise the first other status in lemma order. A body with no lemmas is :gave_up.

Example

Pass a full theory or just the body — bare text is automatically wrapped in theory <name> imports Main begin ... end:

# Full theory
theory = ~S"""
theory Example imports Main begin
lemma "P \<or> \<not> P" by auto
end
"""

# Or just the body (equivalent)
body = ~S"""
lemma "P \<or> \<not> P" by auto
"""

{:ok, session} = AtpClient.Isabelle.open_session()
{:ok, %{status: :theorem}} = AtpClient.Isabelle.prove_theory(session, theory, "Example")
{:ok, %{status: :theorem}} = AtpClient.Isabelle.prove_theory(session, body, "Example")
:ok = AtpClient.Isabelle.close_session(session)

Submitting TPTP/THF problems

query/2 and prove_tptp/3 take an AtpClient.Problem and route it through IsabelleClient.TPTP.isabellize_theory/1 before handing it to the per-lemma path. The bundled TPTP.thy support theory is copied into :local_dir on first use, and the generated theory imports it with unbundle from_TPTP active:

problem = ~S"""
thf(p_type, type, p: $i > $o).
thf(g, conjecture, ! [X: $i]: (p @ X | ~ (p @ X))).
"""

{:ok, result} = AtpClient.Isabelle.query(problem)
result.status           # :theorem
result.details.lemmas   # [%{name: "g", status: :theorem}]

Lemma names propagate from TPTP formula names. Line numbers are not exposed — they would refer to the generated theory, not the input TPTP source. Match on :name for UI attribution.

Cancellation

Death of the process calling query/2 or any of the session helpers is the abort signal. The session is owned by an internal GenServer that monitors the caller; on caller :DOWN it stops the underlying IsabelleClient.Shared, which closes the TCP socket to the Isabelle server. Closing the socket while a use_theories task is in flight aborts that task on the server side — per the Isabelle protocol, the running check is dropped along with the session.

IsabelleClient does not currently expose a way to cancel a single in-flight task while keeping the session open, so cancelled calls pay the session-start cost again on the next request (typically a few seconds for HOL, longer for larger sessions). Callers that need to cancel cheaply should keep the volume of cancelled work small or open multiple sessions in parallel.

Summary

Types

What a proving call returns.

Functions

Stops the active Isabelle session and shuts down the underlying IsabelleClient.Shared GenServer, closing its TCP socket.

Connects to the Isabelle server, starts a session (typically HOL or Main), and returns a Session handle.

Writes theory_text to <local_dir>/<theory_name>.thy, asks the Isabelle server to process it in the given session, and blocks until the task finishes or fails.

Like prove_lemmas/4, but takes an AtpClient.Problem in TPTP/THF instead of an Isabelle theory.

Behaviour entry point: opens a session, runs prove_tptp/3, and closes the session afterwards, even on error.

Convenience wrapper: opens a session, calls prove_lemmas/4, and unconditionally closes the session afterwards (even on error).

Types

result()

@type result() :: {:ok, AtpClient.Result.t()} | {:error, term()}

What a proving call returns.

  • {:ok, %AtpClient.Result{}} — the task finished; see "Results" in the module documentation for what the result carries.
  • {:error, {:isabelle_failed, payload, notes}} — the server reported a FAILED message for the task, such as a theory file it could not load. notes is the list of intermediate NOTE payloads IsabelleClient.Task accumulated before the failure. When the message is "Cannot load theory file", notes is replaced by hints carrying the local_dir and isabelle_dir actually used.
  • other {:error, reason} for connection, session-start, and I/O issues.

Functions

close_session(session)

@spec close_session(AtpClient.Isabelle.Session.t()) :: :ok

Stops the active Isabelle session and shuts down the underlying IsabelleClient.Shared GenServer, closing its TCP socket.

open_session(opts \\ [])

@spec open_session(keyword()) ::
  {:ok, AtpClient.Isabelle.Session.t()} | {:error, term()}

Connects to the Isabelle server, starts a session (typically HOL or Main), and returns a Session handle.

The handle is backed by a private owner process that holds the link to IsabelleClient.Shared for you, so callers do not need to trap exits: a failed connection surfaces as {:error, reason} and a later crash of the Shared process surfaces as a :DOWN if the caller monitors AtpClient.Isabelle.Session.client/1. The owner is also caller-monitored, so if the caller dies the session is shut down cleanly instead of orphaning a remote Isabelle session.

The caller is still responsible for eventually passing the handle to close_session/1 under normal control flow.

Options

  • :host, :port, :password — override config;
  • :session — name of the Isabelle session to start (default from config, typically "HOL");
  • :dirs — list of directories containing ROOT files, forwarded as dirs to session_start so that :session may name a session defined outside the Isabelle distribution. Paths are as seen by the Isabelle server and are passed through verbatim (default from config, else none);
  • :session_start_timeout_ms — how long to wait for the initial session_start to complete (default 120_000).

prove_lemmas(session, theory_text, theory_name, opts \\ [])

@spec prove_lemmas(AtpClient.Isabelle.Session.t(), String.t(), String.t(), keyword()) ::
  result()

Like prove_theory/4, but reads one status per lemma.

The result's details.lemmas holds one entry per lemma <name>: declaration in the body, in that order, each %{name: name, status: status}; the result's own status is the weakest link over them. Unnamed lemma "…" by … declarations are not represented — the classifier buckets messages by the lemma names it finds in the body.

Options

Same as prove_theory/4.

prove_theory(session, theory_text, theory_name, opts \\ [])

@spec prove_theory(AtpClient.Isabelle.Session.t(), String.t(), String.t(), keyword()) ::
  result()

Writes theory_text to <local_dir>/<theory_name>.thy, asks the Isabelle server to process it in the given session, and blocks until the task finishes or fails.

If theory_text does not begin with theory, it is automatically wrapped:

theory <theory_name> imports <imports> begin
<theory_text>
end

The status is read over every message of the task by AtpClient.Isabelle.Verdict.classify/1; the use_theories payload is in result.details.payload.

Options

  • :imports — session/theory to import when auto-wrapping (default "Main");
  • :use_theories_timeout_ms — overall deadline for the task (default from config, 120_000).

Examples

{:ok, %AtpClient.Result{status: :theorem, details: %{payload: %{"ok" => true}}}} =
  AtpClient.Isabelle.prove_theory(session, "lemma p: True by auto\n", "P")

prove_tptp(session, problem, opts \\ [])

Like prove_lemmas/4, but takes an AtpClient.Problem in TPTP/THF instead of an Isabelle theory.

The problem is converted to an Isabelle theory body via IsabelleClient.TPTP.isabellize_theory/1, which maps thf(name, axiom, …) to axiomatization, thf(name, type, …) to consts/typedecl/ type_synonym, and thf(name, theorem|conjecture, …) to lemma. The generated body is preceded by unbundle from_TPTP so that the TPTP/THF notation parses inside Isabelle, then handed to prove_lemmas/4 with imports: "TPTP".

The bundled TPTP.thy support theory is copied into the configured :local_dir on first use so Isabelle's loader can resolve imports "TPTP" from the generated theory file. Existing files are not overwritten.

Result attribution

result.details.lemmas follows the order of the thf(…, conjecture, …) declarations in the input problem and carries the TPTP formula name as :name. Line numbers are intentionally not exposed: they would refer to the generated theory body rather than the caller's TPTP source.

Options

Same as prove_lemmas/4, plus:

  • :theory_name — name to use for the generated Isabelle theory (default: a content-derived AtpClient_<hash> string). Useful for stable filenames across runs.
  • :proof_method — Isabelle tactic appended to each generated lemma (default "by auto"). The isabellizer emits lemma declarations without proofs, which Isabelle treats as unfinished goals; a tactic must be supplied for the goal to close (or be withdrawn). Pass "by metis" for first-order portfolio reasoning, or "sledgehammer nitpick oops" to probe both proof and countersatisfiability; the latter relies on the per-lemma classifier recognising sledgehammer's "found a proof" and nitpick's verdicts directly, so oops is fine — :theorem / :counter_satisfiable / :satisfiable come from the tool messages, not from a theorem name: completion.

Errors

  • {:error, {:tptp_thy_copy_failed, reason}} — could not place TPTP.thy next to the generated theory in :local_dir;
  • {:error, {:tptp_parse, message}} — isabellize_theory/1 raised (the parser is tolerant by design, so this should be rare);
  • everything prove_lemmas/4 can return.

query(problem, opts \\ [])

@spec query(AtpClient.Problem.t(), keyword()) :: result()

Behaviour entry point: opens a session, runs prove_tptp/3, and closes the session afterwards, even on error.

The result's status is the weakest link over the isabellized lemmas and details.lemmas has each lemma's own status.

THF-only

IsabelleClient.TPTP.isabellize_theory/1 in the currently vendored :isabelle_elixir (0.4.1) only recognises thf(…) annotated formulae. FOF/CNF input is silently dropped by the isabellizer, so the generated body has no lemmas and the result is :gave_up — indistinguishable from an honest failed proof. If you need FOF/CNF today, route through the SystemOnTPTP, StarExec, or LocalExec backends instead.

query_lemmas(theory_text, theory_name, opts \\ [])

@spec query_lemmas(String.t(), String.t(), keyword()) :: result()

Convenience wrapper: opens a session, calls prove_lemmas/4, and unconditionally closes the session afterwards (even on error).