# PTC-Lisp Playground

```elixir
# For local dev: run `mix deps.get` in the project root first
repo_root = Path.expand("..", __DIR__)

deps =
  if File.exists?(Path.join(repo_root, "mix.exs")) do
    [{:ptc_runner, path: repo_root}]
  else
    [{:ptc_runner, "~> 0.11.0"}]
  end

Mix.install(deps, consolidate_protocols: false)
```

## Introduction

PTC-Lisp is a small, safe subset of Clojure designed for Programmatic Tool Calling. Programs run in sandboxed BEAM processes with resource limits (1s timeout, 10MB memory).

**Key concepts:**

* `->>` threads data through a pipeline (like Elixir's `|>`)
* `:keyword` accesses map fields (converted to string keys internally)
* `#(...)` is an anonymous function (`%` is its argument), used to build filter predicates
* `tool/tool-name` calls external tools

## Basic Example

Filter expenses and sum amounts:

```elixir
tools = %{
  "get-expenses" => fn _args ->
    [
      %{"category" => "travel", "amount" => 500},
      %{"category" => "food", "amount" => 50},
      %{"category" => "travel", "amount" => 200}
    ]
  end
}

program = ~S|(->> (tool/get-expenses) (filter #(= (:category %) "travel")) (sum-by :amount))|

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)

IO.puts("Travel expenses: #{step.return}")
```

### Step-by-step breakdown

1. `(tool/get-expenses)` - calls the tool, returns list of expense maps
2. `(filter #(= (:category %) "travel"))` - keeps only travel expenses (`#(...)` is an anonymous function; `%` is the item under test)
3. `(sum-by :amount)` - sums the amount field

## PTC-Lisp Extensions

The example above uses `sum-by`, a PTC-Lisp extension not found in standard Clojure. These extensions make common aggregation patterns more concise.

In standard Clojure, the same sum would be written with `map` and `reduce`:

```elixir
program = ~S|(->> (tool/get-expenses) (filter #(= (:category %) "travel")) (map :amount) (reduce +))|

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)

IO.puts("Travel expenses (Clojure style): #{step.return}")
```

**Key PTC-Lisp extensions:**

| Extension              | Clojure Equivalent             | Purpose                       |
| ---------------------- | ------------------------------ | ----------------------------- |
| `(sum-by :field coll)` | `(reduce + (map :field coll))` | Sum a field across collection |
| `(avg-by :field coll)` | Manual calculation             | Average a field               |

Filtering uses standard Clojure anonymous functions — `#(= (:field %) value)`, where `%` is the item under test. These aggregation extensions are designed for LLM code generation: they reduce syntax errors and make intent clearer.

## Key Convention

Tools receive and return **string-keyed maps** (like JSON):

```elixir
# Tool data uses string keys
%{"category" => "travel", "amount" => 500}
```

PTC-Lisp's FlexAccess makes `:keyword` access work transparently with string-keyed data:

```clojure
; These all work on string-keyed maps (PTC-Lisp, not runnable here):
(:category item)                 ; => "travel"
(filter #(= (:category %) ...))  ; matches {"category" "travel"}
(sum-by :amount expenses)        ; sums the "amount" field
```

**Why string keys?**

* Prevents atom table exhaustion from LLM-generated code
* Matches JSON conventions (external APIs, Phoenix params)
* Tools pattern match on string keys: `fn %{"id" => id} -> ... end`

## Working with Variables

Use `let` to bind intermediate results:

```elixir
program = ~S"""
(let [expenses (tool/get-expenses)
      travel (filter #(= (:category %) "travel") expenses)]
  {:count (count travel)
   :total (sum-by :amount travel)
   :avg (avg-by :amount travel)})
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)
step.return
```

## Error Handling

PTC-Lisp provides helpful error messages with hints:

```elixir
# Calling a tool that wasn't provided
bad_program = ~S|(tool/get-expenses)|

case PtcRunner.Lisp.run(bad_program, tools: %{}) do
  {:error, error} -> IO.puts("Error: #{inspect(error)}")
  {:ok, step} -> step.return
end
```

```elixir
# Type error - sum-by needs a collection
bad_program = ~S|(sum-by :amount "not a list")|

case PtcRunner.Lisp.run(bad_program, tools: %{}) do
  {:error, error} -> IO.puts("Error: #{inspect(error)}")
  {:ok, step} -> step.return
end
```

## Data Transformation

Transform and join data from multiple sources:

```elixir
tools = %{
  "get-users" => fn _args ->
    [
      %{"id" => 1, "name" => "Alice", "email" => "alice@example.com"},
      %{"id" => 2, "name" => "Bob", "email" => "bob@example.com"}
    ]
  end,
  "get-orders" => fn _args ->
    [
      %{"user-id" => 1, "product" => "Laptop", "total" => 1200},
      %{"user-id" => 2, "product" => "Mouse", "total" => 25},
      %{"user-id" => 1, "product" => "Keyboard", "total" => 150}
    ]
  end
}

program = ~S"""
(let [users (tool/get-users)
      orders (tool/get-orders)
      high-value (filter #(> (:total %) 100) orders)]
  (->> high-value
       (mapv (fn [order]
               (let [user (first (filter #(= (:id %) (:user-id order)) users))]
                 {:customer (:name user)
                  :product (:product order)
                  :total (:total order)})))))
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: tools)
step.return
```

## Advanced: Grouping and Aggregation

Group expenses by category and compute totals:

```elixir
expenses_tools = %{
  "get-expenses" => fn _args ->
    [
      %{"category" => "travel", "amount" => 500},
      %{"category" => "food", "amount" => 50},
      %{"category" => "travel", "amount" => 200},
      %{"category" => "food", "amount" => 75}
    ]
  end
}

program = ~S"""
(let [expenses (tool/get-expenses)
      by-category (group-by :category expenses)]
  (->> (keys by-category)
       (mapv (fn [cat]
               {:category cat
                :total (sum-by :amount (get by-category cat))
                :count (count (get by-category cat))}))))
"""

{:ok, step} = PtcRunner.Lisp.run(program, tools: expenses_tools)
step.return
```

## Learn More

* [PTC-Lisp Specification](https://hexdocs.pm/ptc_runner/ptc-lisp-specification.html) - Complete language reference
* [SubAgent Getting Started](https://hexdocs.pm/ptc_runner/subagent-getting-started.html) - Build LLM-powered agents
* [LLM Agent Livebook](ptc_runner_llm_agent.livemd) - Interactive agent example
