Powered by AppSignal & Oban Pro

IsabelleClient.Raw

IsabelleClientRaw.livemd

IsabelleClient.Raw

Intro

This notebook uses IsabelleClient.Raw. It exposes the TCP socket, keeps no session state, and leaves async task waiting explicit. It also exposes some basic server management functionality.

Read IsabelleClient.livemd first if you are new to the library. The raw API is useful when you want to see or control the Isabelle server protocol directly: you send a command, receive a task id, then wait for the task yourself.

When using the published package, replace the dependency with {:isabelle_elixir, "~> 0.3"}.

Mix.install([
  {:isabelle_elixir, path: Path.expand("..", __DIR__)}
])
IsabelleClient.Server.executable()

Server Management

Resident Isabelle servers are named. list_servers/0 shows local resident servers known to Isabelle, and kill_server/1 can force-kill one by name. This is mainly useful after interrupted runs.

{:ok, servers_before_demo} = IsabelleClient.Raw.list_servers()
servers_before_demo

We start two short-lived servers. Passing port 0 asks Isabelle to choose a free local port.

kill_demo_names =
  for n <- 1..2 do
    name = "livebook_raw_kill_demo_#{n}_#{System.unique_integer([:positive])}"
    {:ok, _server} = IsabelleClient.Raw.new_server(name, 0)
    name
  end
{:ok, servers_after_start} = IsabelleClient.Raw.list_servers()

Enum.filter(servers_after_start, &amp;(&amp;1.name in kill_demo_names))
kill_results = Enum.map(kill_demo_names, &amp;IsabelleClient.Raw.kill_server/1)
{:ok, servers_after_kill} = IsabelleClient.Raw.list_servers()

%{
  kill_statuses: Enum.map(kill_results, fn {_output, status} -> status end),
  remaining_demo_servers: Enum.filter(servers_after_kill, &amp;(&amp;1.name in kill_demo_names))
}

Let’s leave one server running for the rest of the tutorial:

server_name = "livebook_raw_#{System.unique_integer([:positive])}"
{:ok, server} = IsabelleClient.Raw.new_server(server_name, 0)

Session-less Commands

Let’s connect to a running server (running locally or elsewhere):

{:ok, socket} = IsabelleClient.Raw.connect(server["password"], server["host"], server["port"])

Using the socket connection we can invoke some synchronous (“sessionless”) commands.

{:ok, commands} = IsabelleClient.Raw.help(socket)

payload = %{client: "raw", framed: String.duplicate("x", 120)}
{:ok, %{"client" => "raw", "framed" => framed}} = IsabelleClient.Raw.echo(socket, payload)

{:ok, %{"via" => "command/4"}} = IsabelleClient.Raw.command(socket, "echo", %{via: "command/4"})

command/4 is the escape hatch for server commands without a dedicated helper. It sends one request and waits for one OK or ERROR response. cancel_task/2 sends the protocol-level cancellation command; cancelling an unknown task is harmless on the Isabelle side.

{:ok, nil} = IsabelleClient.Raw.cancel_task(socket, "00000000-0000-0000-0000-000000000000")

Session Tasks

Asynchronous commands first return OK {"task": ...}. Raw helpers return that task id as an %IsabelleClient.Task{status: :running}. Call await_task/3 to collect NOTE messages and the final FINISHED or FAILED payload.

{:ok, build_task} =
  IsabelleClient.Raw.async_command(socket, "session_build", session: "HOL", verbose: true)

{:ok, build_task} = IsabelleClient.Raw.await_task(socket, build_task, 120_000)

%{
  status: build_task.status,
  ok?: build_task.result["ok"],
  messages: IsabelleClient.messages(build_task)
}

Dedicated helpers such as start_session/3 and use_theories/3 are thin wrappers around async_command/4. start_session/3 accepts Isabelle session_start arguments such as session, preferences, options, dirs, include_sessions, verbose, and print_mode.

{:ok, start_task} =
  IsabelleClient.Raw.start_session(socket,
    session: "HOL",
    verbose: true,
    print_mode: ["ASCII"]
  )

{:ok, start_task} = IsabelleClient.Raw.poll_status(socket, start_task, 120_000)

session_id = IsabelleClient.extract_session(start_task)

%{
  status: start_task.status,
  session_id: session_id,
  messages: IsabelleClient.messages(start_task)
}

Check A Theory

The raw socket does not remember an active session. Every session command that needs a live session must receive session_id explicitly.

theory_dir =
  Path.join(System.tmp_dir!(), "isabelle_elixir_livebook_raw_#{System.unique_integer([:positive])}")

File.mkdir_p!(theory_dir)

File.write!(Path.join(theory_dir, "RawExample.thy"), """
theory RawExample imports Main
begin

lemma "x = x"
  sledgehammer
  by simp

lemma "xs @ [] = xs"
  sledgehammer
  by simp

end
""")
{:ok, use_task} =
  IsabelleClient.Raw.use_theories(socket,
    session_id: session_id,
    theories: ["RawExample"],
    master_dir: theory_dir,
    pretty_margin: 80.0,
    unicode_symbols: true,
    export_pattern: "*",
    check_delay: 0.1,
    check_limit: 100,
    watchdog_timeout: 60.0,
    nodes_status_delay: 0.5
  )

{:ok, use_task} = IsabelleClient.Raw.await_task(socket, use_task, 120_000)
true = use_task.result["ok"]

The result helpers work with raw-socket task results too. Result.decode/1 turns common Isabelle result maps into small Elixir structs when that is useful.

typed = IsabelleClient.Result.decode(use_task)

%{
  typed_ok?: typed.ok,
  node_names: Enum.map(IsabelleClient.nodes(use_task), &amp; &amp;1.node_name),
  messages: IsabelleClient.messages(use_task),
  sledgehammer_lines: IsabelleClient.messages(use_task, line: [5, 9]),
  proof_lines: IsabelleClient.messages(use_task, line: [6, 10]),
  first_proof_at_offset:
    use_task
    |> IsabelleClient.diagnostics(line: 6)
    |> List.first()
    |> get_in(["pos", "offset"])
    |> then(&amp;IsabelleClient.messages(use_task, line: 6, offset: &amp;1))
}

Build And Start A Custom Session

The raw profile is also the clearest place to see Isabelle’s session_build and session_start arguments. session_start accepts the session_build arguments plus print_mode.

We first write a minimal Isabelle session: one ROOT file and one theory.

custom_suffix = System.unique_integer([:positive])
custom_session = "RawBuild#{custom_suffix}"
custom_dir = Path.join(System.tmp_dir!(), "isabelle_elixir_livebook_raw_build_#{custom_suffix}")
File.mkdir_p!(custom_dir)

File.write!(Path.join(custom_dir, "ROOT"), """
session "#{custom_session}" = "HOL" +
  options [document = false, show_question_marks = false]
  theories
    RawBuildTheory
""")

File.write!(Path.join(custom_dir, "RawBuildTheory.thy"), """
theory RawBuildTheory imports Main
begin

lemma raw_build_example: "x = x"
  by simp

end
""")

Build the reusable session image first.

{:ok, custom_build_task} =
  IsabelleClient.Raw.build_session(socket,
    session: custom_session,
    dirs: [custom_dir],
    options: ["document=false", "show_question_marks=false"],
    include_sessions: [],
    preferences: "",
    verbose: true
  )

{:ok, custom_build_task} =
  IsabelleClient.Raw.await_task(socket, custom_build_task, 120_000)

%{
  result: IsabelleClient.session_build_result(custom_build_task),
  messages: IsabelleClient.messages(custom_build_task)
}

Start a live session from the built image. Keep dirs, so Isabelle can resolve the custom session name.

{:ok, custom_start_task} =
  IsabelleClient.Raw.start_session(socket,
    session: custom_session,
    dirs: [custom_dir],
    options: ["document=false"],
    include_sessions: [],
    preferences: "",
    verbose: true,
    print_mode: ["ASCII"]
  )

{:ok, custom_start_task} =
  IsabelleClient.Raw.await_task(socket, custom_start_task, 120_000)

custom_session_id = IsabelleClient.extract_session(custom_start_task)

%{
  session_id: custom_session_id,
  messages: IsabelleClient.messages(custom_start_task)
}

Use the returned session id explicitly. Raw commands do not remember an active session for you.

File.write!(Path.join(custom_dir, "RawBuildUse.thy"), """
theory RawBuildUse imports RawBuildTheory
begin

lemma raw_build_use: "x = x"
  by (rule raw_build_example)

end
""")

{:ok, custom_use_task} =
  IsabelleClient.Raw.use_theories(socket,
    session_id: custom_session_id,
    theories: ["RawBuildUse"],
    master_dir: custom_dir
  )

{:ok, custom_use_task} =
  IsabelleClient.Raw.await_task(socket, custom_use_task, 120_000)

%{
  ok?: custom_use_task.result["ok"],
  messages: IsabelleClient.messages(custom_use_task)
}

Stop And Shutdown

# Stop the custom session before the original one.
{:ok, custom_stop_task} = IsabelleClient.Raw.stop_session(socket, custom_session_id)
{:ok, custom_stop_task} = IsabelleClient.Raw.await_task(socket, custom_stop_task, 120_000)

{:ok, stop_task} = IsabelleClient.Raw.stop_session(socket, session_id)
{:ok, stop_task} = IsabelleClient.Raw.await_task(socket, stop_task, 120_000)

{stop_task.status, custom_stop_task.status}
{:ok, nil} = IsabelleClient.Raw.shutdown_server(socket)
:ok = IsabelleClient.Raw.close(socket)
:ok