Current section

Files

Jump to
isabelle_elixir lib isabelle_client_mini.ex
Raw

lib/isabelle_client_mini.ex

defmodule IsabelleClientMini do
@moduledoc """
Minimal stateless client for the Isabelle server.
The module intentionally exposes the TCP socket and keeps no process state.
It does, however, implement Isabelle's real line-message framing: replies may
be either a single line (`OK ...`) or a byte length line followed by exactly
that many bytes.
"""
alias IsabelleClient.Protocol
alias IsabelleClient.Protocol.Response
alias IsabelleClient.Server
alias IsabelleClient.Task
@default_host "127.0.0.1"
@default_port 9999
@timeout 30_000
@doc "Starts a local resident Isabelle server via `isabelle server`."
def new_server(name \\ "elixir", port \\ @default_port), do: Server.start(name, port)
@doc "Lists local resident Isabelle servers known to Isabelle."
def list_servers, do: Server.list()
@doc "Force-kills a local resident Isabelle server by name."
def kill_server(name), do: Server.kill(name)
@doc "Connects to an Isabelle server and authenticates with its password."
def connect(password, host \\ @default_host, port \\ @default_port, timeout \\ @timeout) do
with {:ok, socket} <-
:gen_tcp.connect(
to_charlist(host),
port,
[:binary, active: false, nodelay: true],
timeout
),
:ok <- Protocol.send(socket, Protocol.line_message(password)),
{:ok, %Response{type: :ok}} <- Protocol.recv(socket, timeout) do
{:ok, socket}
else
{:ok, response} -> {:error, {:authentication_failed, response}}
{:error, _} = error -> error
end
end
@doc "Closes a raw Isabelle TCP socket."
def close(socket), do: :gen_tcp.close(socket)
@doc "Receives one framed Isabelle server response."
def recv(socket, timeout \\ @timeout), do: Protocol.recv(socket, timeout)
@doc "Runs a synchronous Isabelle server command and returns its `OK` body."
def command(socket, name, arg \\ nil, timeout \\ @timeout) do
with :ok <- Protocol.send(socket, Protocol.command(name, arg)),
{:ok, response} <- Protocol.recv(socket, timeout) do
Protocol.ok_body(response)
end
end
@doc "Starts an asynchronous Isabelle command and returns an `%IsabelleClient.Task{}`."
def async_command(socket, name, arg, timeout \\ @timeout) do
with :ok <- Protocol.send(socket, Protocol.command(name, arg)),
{:ok, response} <- Protocol.recv(socket, timeout),
{:ok, task_id} <- Protocol.task_id(response) do
{:ok, Task.new(task_id)}
end
end
@doc "Waits for an asynchronous Isabelle task to finish or fail."
def await_task(socket, task_or_id, timeout \\ :infinity)
def await_task(socket, %Task{id: id} = task, timeout) do
await_task(socket, id, timeout, task)
end
def await_task(socket, task_id, timeout) when is_binary(task_id) do
await_task(socket, task_id, timeout, Task.new(task_id))
end
defp await_task(socket, task_id, timeout, task) when is_binary(task_id) do
deadline = deadline(timeout)
do_await_task(socket, task, deadline)
end
@doc "Round-trips a JSON value through Isabelle's `echo` command."
def echo(socket, value), do: command(socket, "echo", value)
@doc "Returns the server command names supported by Isabelle."
def help(socket), do: command(socket, "help")
@doc "Requests cancellation of an Isabelle asynchronous task."
def cancel_task(socket, task_id), do: command(socket, "cancel", %{"task" => task_id})
@doc "Asks the Isabelle server process to shut down."
def shutdown_server(socket), do: command(socket, "shutdown")
@doc "Starts an Isabelle `session_build` task."
def build_session(socket, args), do: async_command(socket, "session_build", args)
@doc "Starts an Isabelle `session_start` task."
def start_session(socket, args), do: async_command(socket, "session_start", args)
@doc "Starts an Isabelle `session_stop` task for a session id."
def stop_session(socket, session_id),
do: async_command(socket, "session_stop", %{"session_id" => session_id})
@doc "Starts an Isabelle `use_theories` task."
def use_theories(socket, args), do: async_command(socket, "use_theories", args)
@doc "Runs Isabelle `purge_theories` for an active session."
def purge_theories(socket, args), do: command(socket, "purge_theories", args)
@doc "Alias for `await_task/3`, kept for the original Mini workflow."
def poll_status(socket, task_or_id, timeout \\ :infinity)
def poll_status(socket, %Task{} = task, timeout),
do: await_task(socket, task, timeout)
def poll_status(socket, task_id, timeout) when is_binary(task_id),
do: await_task(socket, task_id, timeout)
@doc "Extracts the `session_id` from a finished session-start result."
def extract_session(%Task{status: :finished, result: %{"session_id" => session_id}}),
do: session_id
def extract_session(%{"session_id" => session_id}), do: session_id
@doc "Extracts user-facing theory messages from a finished `use_theories` result."
def extract_results(%Task{status: :finished, result: result}), do: extract_results(result)
def extract_results(%{"nodes" => [node | _]}) do
node
|> Map.get("messages", [])
|> Enum.map(&Map.get(&1, "message", ""))
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")
end
def extract_results(_), do: ""
defp do_await_task(socket, %Task{id: id, notes: notes} = task, deadline) do
with {:ok, response} <- Protocol.recv(socket, remaining(deadline)) do
response_task = task_id(response.body)
cond do
response.type == :note and response_task in [nil, id] ->
do_await_task(socket, %{task | notes: notes ++ [response.body]}, deadline)
response.type == :finished and response_task == id ->
{:ok, %{task | status: :finished, result: response.body}}
response.type == :failed and response_task == id ->
{:error, %{task | status: :failed, result: response.body}}
true ->
do_await_task(socket, task, deadline)
end
end
end
defp task_id(%{"task" => id}), do: id
defp task_id(_), do: nil
defp deadline(:infinity), do: :infinity
defp deadline(timeout), do: System.monotonic_time(:millisecond) + timeout
defp remaining(:infinity), do: :infinity
defp remaining(deadline) do
max(deadline - System.monotonic_time(:millisecond), 0)
end
end