# `PtcRunner.Lisp`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/lisp.ex#L1)

Execute PTC programs written in Lisp DSL (Clojure subset).

PTC-Lisp enables LLMs to write safe programs that orchestrate tools and transform
data. Unlike raw code execution (Python, JavaScript), PTC-Lisp provides safety by
design: no filesystem/network access, no unbounded recursion, and deterministic
execution in isolated BEAM processes with resource limits.

See the [PTC-Lisp Specification](ptc-lisp-specification.md) for the complete
language reference.

## Tool Registration

Tools are functions that receive a map of arguments and return results.
Note: tool names use kebab-case in Lisp (e.g., `"get-user"` not `"get_user"`):

    tools = %{
      "get-user" => fn %{"id" => id} -> MyApp.Users.get(id) end,
      "search" => fn %{"query" => q} -> MyApp.Search.run(q) end
    }

    PtcRunner.Lisp.run(~S|(tool/get-user {:id 123})|, tools: tools)

**Contract:**
- Receives: `map()` of arguments (may be empty `%{}`)
- Returns: Any Elixir term (maps, lists, primitives)
- Should not raise (return `{:error, reason}` for errors)

# `format_error`

```elixir
@spec format_error(term()) :: String.t()
```

Format an error tuple into a human-readable string.

Useful for displaying errors to users or feeding back to LLMs for retry.

## Examples

    iex> PtcRunner.Lisp.format_error({:parse_error, "unexpected token"})
    "Parse error: unexpected token"

    iex> PtcRunner.Lisp.format_error({:eval_error, "undefined variable: x"})
    "Eval error: undefined variable: x"

# `format_value`

```elixir
@spec format_value(
  term(),
  keyword()
) :: {String.t(), boolean()}
```

Format a PTC-Lisp value as Clojure-style syntax for display.

This is the public wrapper around `PtcRunner.Lisp.Format.to_clojure/2` used by
LLM-facing renderers and embedding applications.

Returns `{formatted_string, truncated?}`.

## Examples

    iex> PtcRunner.Lisp.format_value(%{count: 2, ids: [1, 2]})
    {"{:count 2 :ids [1 2]}", false}

    iex> PtcRunner.Lisp.format_value([1, 2, 3], limit: 2)
    {"[1 2 ...] (2/3)", true}

# `run`

```elixir
@spec run(
  String.t(),
  keyword()
) :: {:ok, PtcRunner.Step.t()} | {:error, PtcRunner.Step.t()}
```

Run a PTC-Lisp program.

## Parameters

- `source`: PTC-Lisp source code as a string
- `opts`: Keyword list of options
  - `:context` - Initial context map (default: %{})
  - `:memory` - Initial memory map (default: %{})
  - `:turn_history` - Prior turn return values, oldest first, used by
    `*1`, `*2`, and `*3` (default: `[]`)
  - `:tools` - Map of tool names to functions (default: %{})
  - `:signature` - Optional signature string for return value validation
  - `:float_precision` - Number of decimal places for floats in result (default: nil = full precision)
  - `:timeout` - Timeout in milliseconds for entire sandbox execution (default: 1000)
  - `:compile_timeout` - Timeout in milliseconds for the compile phase (parse + analyze) (default: 5000)
  - `:pmap_timeout` - Timeout in milliseconds per pmap/pcalls task (default: 5000). Increase for LLM-backed tools.
  - `:pmap_max_concurrency` - Local pmap/pcalls scheduling window — max tasks one call keeps in flight (default: `System.schedulers_online() * 2`). Reduce to avoid overflowing connection pools. The HARD aggregate cap is `:max_parallel_workers`.
  - `:max_heap` - Program heap budget in words ABOVE the measured
    environment baseline (default: 1_250_000). Host-provided data
    (context, `:memory`, tool closures, the parsed program) is measured
    after spawn and excluded from this budget — see
    `PtcRunner.Sandbox` for the re-baseline semantics.
  - `:setup_max_heap` - Hard heap ceiling in words while the host
    environment is copied into the sandbox, before the re-baseline
    (default: `4 × max_heap`). Callers granting large tools/memory must
    raise this explicitly; exceeding it fails with a setup-phase
    `:memory_exceeded` error.
  - `:worker_max_heap` - Fixed `max_heap_size` (words) for every pmap/pcalls worker, top-level and nested (default: the `:max_heap` value)
  - `:max_parallel_workers` - Global cap on pmap/pcalls worker processes alive at once across the whole run, at any nesting depth (default: 8). Aggregate live parallel heap ≈ `max_parallel_workers * worker_max_heap`. A pmap/pcalls that cannot get a slot fails with `:parallel_capacity_exceeded`.
  - `:max_symbols` - Max unique symbols/keywords allowed (default: 10_000)
  - `:max_program_bytes` - Max source code size in bytes (default: 1_000_000)
  - `:max_print_length` - Max characters per `println` call (default: 2000)
  - `:filter_context` - Filter context to only include accessed data keys (default: true)
  - `:budget` - Budget info map for `(budget/remaining)` introspection (default: nil)
  - `:prelude` - A compiled `%PtcRunner.Lisp.Prelude{}` artifact, a prelude
    SOURCE string, or a list of source-bearing selection maps accepted by
    `PtcRunner.Lisp.Prelude.Bundle.compile/1` to attach before user code
    (Capability Prelude V1). Source selections are concatenated and compiled
    once in explicit order after duplicate namespace rejection. The attached
    prelude's protected namespaces and public export table are consulted by
    the analyzer/evaluator so qualified prelude calls (e.g. `crm/get-user`)
    resolve, while private helpers stay user-invisible. Compile/attach
    failures return `{:error, Step}`. When a `:runtime` is also supplied,
    attach-time `requires` validation runs first. (default: nil)
  - `:runtime` - Selected upstream runtime handle used for attach-time prelude
    `requires` validation. When `nil`, prelude `requires` are not validated
    against any upstream (the configured `:tools` map still guards actual tool
    surfaces). (default: nil)
  - `:trace_context` - Trace context for nested agent tracing (default: nil)
  - `:caller` - Closed-set tag for telemetry. One of `:in_process_v1`,
    `:text_mode`, or `:mcp` (default: `:in_process_v1`). Pure
    instrumentation: attached to `[:ptc_runner, :lisp, :execute, *]`
    events and otherwise discarded. Out-of-set values raise
    `ArgumentError`.
  - `:profile` - Closed-set telemetry tag for the calling profile.
    One of `:mcp_no_tools`, `:mcp_aggregator`, `:in_process_v1`, or
    `:text_mode`, or `nil` (default). Pure instrumentation: attached
    to `[:ptc_runner, :lisp, :execute, *]` events and otherwise
    discarded. Out-of-set values raise `ArgumentError`. The MCP v1
    handler passes `:mcp_no_tools`; the aggregator (Phase 1a)
    flips it to `:mcp_aggregator`. See
    `Plans/ptc-runner-mcp-aggregator.md` §11.5.

## Telemetry

`run/2` is wrapped in `:telemetry.span/3` and emits the following events:

- `[:ptc_runner, :lisp, :execute, :start]` — measurements
  `monotonic_time`, `system_time`; metadata `caller`, `profile`,
  `program_bytes`, `signature_supplied?`.
- `[:ptc_runner, :lisp, :execute, :stop]` — measurements `duration`,
  `monotonic_time`, `result_bytes`, `prints_count`; metadata `caller`,
  `profile`, `program_bytes`, `signature_supplied?`.
- `[:ptc_runner, :lisp, :execute, :exception]` — measurements `duration`,
  `monotonic_time`; metadata `caller`, `profile`, `program_bytes`,
  `signature_supplied?`, `kind`, `reason`, `stacktrace`.

## Return Value

On success, returns:
 - `{:ok, Step.t()}` with:
   - `step.return`: The value returned to the caller
   - `step.memory`: Complete memory state after execution
   - `step.usage`: Execution metrics (`duration_ms`, `memory_bytes`,
     `eval_reductions`)

On error, returns:
- `{:error, Step.t()}` with:
  - `step.fail.reason`: Error reason atom
  - `step.fail.message`: Human-readable error description
  - `step.memory`: Memory state at time of error

## Memory Contract

The top-level program value passes through to `step.return` **unchanged** —
there is no implicit map merge and no special `:return` key handling. Storage
is **explicit**: `(def x v)` persists `v` in memory (`step.memory["x"]`), and
that memory survives across turns within a single `SubAgent` run.

`:turn_history` is separate from memory. Hosts pass a list of prior
successful `step.return` values in chronological order; `*1` reads the most
recent value, `*2` the previous value, and `*3` the third-most-recent value.
Missing history reads return `nil`. `run/2` does not mutate the supplied
history; callers that want REPL semantics should append `step.return` only
after a successful run and keep their chosen bounded depth. For direct
embedding use, `PtcRunner.Session` implements this contract with a default
depth of 3.

**Related modules:**
- `PtcRunner.SubAgent.Loop` - Uses this contract to persist memory across turns
- `PtcRunner.Session` - Public stateful embedding wrapper for memory and turn history
- `PtcRunner.Lisp.Eval` - Evaluates programs with user_ns (memory) symbol resolution

## Float Precision

When `:float_precision` is set, all floats in the result are rounded to that many decimal places.
This is useful for LLM-facing applications where excessive precision wastes tokens.

    # Full precision (default)
    {:ok, step} = PtcRunner.Lisp.run("(/ 10 3)")
    step.return
    #=> 3.3333333333333335

    # Rounded to 2 decimals
    {:ok, step} = PtcRunner.Lisp.run("(/ 10 3)", float_precision: 2)
    step.return
    #=> 3.33

## Resource Limits

Lisp programs execute with configurable timeout and memory limits:

    PtcRunner.Lisp.run(source, timeout: 5000, max_heap: 5_000_000)

Exceeding limits returns an error:
- `{:error, {:timeout, ms}}` - execution exceeded timeout
- `{:error, {:memory_exceeded, info}}` - heap limit exceeded; `info` is a
  diagnostics map (`:phase`, `:limit_bytes`, `:baseline_bytes`,
  `:budget_bytes`) where `phase: :eval` marks a program over its budget and
  `phase: :setup` a granted environment over the setup ceiling, surfaced in
  `Step.fail.details`

## Context Filtering

By default, PTC-Lisp performs static analysis to identify which `data/xxx` keys are accessed
by a program, then filters the context to only include those datasets. This significantly
reduces memory pressure when the context contains large datasets that aren't used.

    # Only products is loaded into the sandbox, orders/employees are filtered out
    ctx = %{"products" => large_list, "orders" => large_list, "employees" => large_list}
    PtcRunner.Lisp.run("(count data/products)", context: ctx)

Scalar context values (strings, numbers, nil) are always preserved as they typically
represent metadata like prompts or configuration.

Disable filtering if you need all context available (e.g., for dynamic data access):

    PtcRunner.Lisp.run(source, context: ctx, filter_context: false)

See `PtcRunner.Lisp.DataKeys` for the static analysis implementation.

# `validate`

```elixir
@spec validate(
  String.t(),
  keyword()
) :: :ok | {:error, [String.t()]}
```

Validate PTC-Lisp source code without executing it.

Parses and analyzes the source, then checks for undefined variables.
Returns `:ok` if valid, or `{:error, messages}` with a list of error strings.

Accepts optional keyword options to configure compile-phase limits:

  * `:compile_timeout` - Timeout in ms for bounded compile (default: 5000)
  * `:max_heap` - Max heap words for bounded compile (default: 1_250_000)
  * `:max_program_bytes` - Max source size in bytes (default: 1_000_000)

## Examples

    iex> PtcRunner.Lisp.validate("(and (map? data/result) (> (count data/result) 0))")
    :ok

    iex> PtcRunner.Lisp.validate("(and (map? foo) true)")
    {:error, ["foo"]}

    iex> PtcRunner.Lisp.validate("(let [x 1] (> x 0))")
    :ok

---

*Consult [api-reference.md](api-reference.md) for complete listing*
