livebook_examples/IsabelleClientMini.livemd

# IsabelleClientMini

`IsabelleClientMini` is the bare-bones client. It exposes the TCP socket and
keeps no state. That is useful when you want a tiny protocol building block or
you want to own all lifecycle decisions yourself.

This notebook does the following:

* start and discover a local Isabelle server
* connect with the server password
* run `help`, `echo`, and raw `command`
* exercise `cancel`, `session_build`, `session_start`, `use_theories`,
  `purge_theories`, `session_stop`, and `shutdown`
* extract messages from the finished theory task

## Setup

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

```elixir
Mix.install([
  {:isabelle_elixir, path: Path.expand("..", __DIR__)}
])
```

You need to make sure that the `isabelle` executable is in the `PATH`.

```elixir
System.find_executable("isabelle")
```

## Start And Discover A Server

Passing port `0` asks Isabelle to choose a free local port. The returned map is
the connection descriptor: host, port, server name, and password.

```elixir
server_name = "livebook_mini_#{System.unique_integer([:positive])}"
{:ok, [server]} = IsabelleClientMini.new_server(server_name, 0)
server
```

`list_servers/0` goes through Isabelle's local server registry. This is handy
when a server was started outside of Elixir.

```elixir
{:ok, servers} = IsabelleClientMini.list_servers()
```

## Connect

Mini returns a raw socket. All later calls receive that socket explicitly.

```elixir
{:ok, socket} =
  IsabelleClientMini.connect(server["password"], server["host"], server["port"])
socket
```

## Synchronous Commands

`help` is a simple command. Isabelle may return the response as a short line or
as a byte-length-framed message; Mini hides that framing detail.

```elixir
{:ok, commands} = IsabelleClientMini.help(socket)
commands
```

`echo` is useful because it round-trips arbitrary JSON values. The long string
forces Isabelle's long-message framing path.

```elixir
{:ok, echoed} =
  IsabelleClientMini.echo(socket, %{
    "client" => "mini",
    "framed" => String.duplicate("x", 120)
  })
echoed
```

The raw `command/3` escape hatch is still there for server commands that do not
deserve their own helper yet.

```elixir
{:ok, %{"client" => "mini", "raw" => true}} =
  IsabelleClientMini.command(socket, "echo", %{"client" => "mini", "raw" => true})
```

Canceling an unknown task is a no-op on the Isabelle side, but it proves the
`cancel` command's empty `OK` body is handled correctly.

```elixir
{:ok, nil} =
  IsabelleClientMini.cancel_task(socket, "00000000-0000-0000-0000-000000000000")
```

## Build And Start A Session

The next commands are asynchronous. Isabelle first returns `OK {"task": ...}`,
then streams `NOTE` messages, and finally returns `FINISHED` or `FAILED`.

```elixir
{:ok, build_task} = IsabelleClientMini.build_session(socket, %{"session" => "HOL"})
{:ok, build_task} = IsabelleClientMini.await_task(socket, build_task, 120_000)

{build_task.status, build_task.result["ok"], length(build_task.notes)}
```

```elixir
{:ok, start_task} = IsabelleClientMini.start_session(socket, %{"session" => "HOL"})
{:ok, start_task} = IsabelleClientMini.poll_status(socket, start_task, 120_000)

session_id = IsabelleClientMini.extract_session(start_task)
{start_task.status, session_id, Enum.map(start_task.notes, & &1["message"])}
```

## Check A Theory

Isabelle reads theories from disk, so the notebook creates a temporary directory.

```elixir
theory_dir =
  Path.join(System.tmp_dir!(), "isabelle_elixir_livebook_mini_#{System.unique_integer([:positive])}")
File.mkdir_p!(theory_dir)
theory_dir
```

We write a tiny `.thy` file to the temporary directory and ask the session to check it.

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

theorem "x = x"
  sledgehammer by simp

proposition "x = y"
  nitpick oops

end
""")
```

```elixir
{:ok, use_task} =
  IsabelleClientMini.use_theories(socket, %{
    "session_id" => session_id,
    "theories" => ["Example"],
    "master_dir" => theory_dir
  })

{:ok, use_task} = IsabelleClientMini.await_task(socket, use_task, 120_000)
{use_task.status, use_task.result["ok"], use_task.result["errors"]}
```

`extract_results/1` gives the user-facing messages from the checked node.

```elixir
results = IsabelleClientMini.extract_results(use_task)
```

`purge_theories` is synchronous and returns the purged/retained node lists.

```elixir
{:ok, purge_result} =
  IsabelleClientMini.purge_theories(socket, %{
    "session_id" => session_id,
    "theories" => ["Example"],
    "master_dir" => theory_dir
  })

{purge_result["purged"], purge_result["retained"]}
```

## Stop And Shutdown

Stop the Isabelle session cleanly, then shut down the resident server through
the protocol.

```elixir
{:ok, stop_task} = IsabelleClientMini.stop_session(socket, session_id)
{:ok, stop_task} = IsabelleClientMini.await_task(socket, stop_task, 120_000)

{stop_task.status, stop_task.result}
```

```elixir
{:ok, nil} = IsabelleClientMini.shutdown_server(socket)
:ok = IsabelleClientMini.close(socket)
```

`kill_server/1` is harmless after protocol shutdown and useful when rerunning
only part of the notebook.

```elixir
IsabelleClientMini.kill_server(server_name)
```