# PTC-Lisp Language Specification

**Related docs:**
- [Clojure Conformance Gaps](clojure-conformance-gaps.md) — tracked deviations from Clojure (bugs, missing features, intentional divergences)
- [Function Reference](function-reference.md) — supported PTC-Lisp functions and special forms

---

## 1. Overview

PTC-Lisp is a small, safe, deterministic subset of Clojure designed for Programmatic Tool Calling. Programs are expressions that transform data through pipelines of operations. Multiple top-level expressions are supported with implicit `do` semantics.

### Execution Model

A PTC-Lisp program is a **pure function** of `(memory, ctx) → result`:

- **Input**: Persistent memory from previous turns + current request context
- **Output**: A result value that may update persistent memory
- **Semantics**: Functional, transactional, all-or-nothing

This design enables safe execution in **agentic LLM loops** where programs are generated, executed, and refined across multiple turns.

### Design Goals

1. **LLM-friendly**: Easy for language models to generate correctly
2. **Safe**: No side effects, resource-limited execution, no system access
3. **Compact**: Minimal syntax, high information density
4. **Verifiable**: Can be validated against real Clojure for correctness
5. **Expressive**: Sufficient for common data transformation tasks
6. **Transactional**: All-or-nothing memory updates, safe for retry loops

### Design Philosophy

Clojure compatibility is the default — but it is not the top-level
goal. The top-level goal is deterministic, bounded, recoverable data
transformation inside an agent loop. When the two conflict, four
rules decide:

1. **No `try` / `catch`.** Raising terminates the program and there is
   no recovery path. So Clojure-named helpers prefer **signal values**
   (`nil`, `""`, `false`, an empty collection) when external input is
   bad and the caller can reasonably continue:
   `(parse-long "abc")` returns `nil`, not a `NumberFormatException`.
2. **Eager, not lazy.** Inputs must be finite and bounded; programs
   run under wall-clock and memory caps.
3. **Java-named methods follow Java-compatible conventions where those
   conventions are meaningful in PTC-Lisp.** The dot prefix signals
   "Java idiom expected": familiar names, arities, not-found values
   (`.indexOf` returns `-1`), and bounds errors for invalid indexes
   (`.substring` raises on out-of-range; `.length`/`.indexOf` raise on
   non-string-like receivers). They do **not** preserve Java
   object/type distinctions PTC-Lisp intentionally does not model — e.g.
   `Character` vs one-character `String`, so `(.toUpperCase \a)` returns
   `"A"` rather than raising (see DIV-40/DIV-41). Java is a compatibility
   heuristic, not the design owner. `subs`, `parse-long`, `get` follow
   the safer-for-sandbox signal pattern.
4. **Properties of input data may signal; properties of the program
   raise.** `(parse-long "abc")` signals (bad data); `(+ 1 nil)`,
   wrong arity, or unknown symbols raise (bad program — fix it).

Tracked Clojure divergences live in
[Clojure Conformance Gaps](clojure-conformance-gaps.md) under
`DIV-*` entries.

### Classifying a Divergence

A divergence is filed by *area* — `GAP-S*` (semantics), `GAP-F*` (special form),
`GAP-C*` (core fn), `GAP-J*` (Java method), or as a `DIV-*` entry when it is
intentional — and given a *disposition*, recorded in
[Clojure Conformance Gaps](clojure-conformance-gaps.md):

- **BUG** — PTC-Lisp implements the form but is wrong: a silent wrong result, or
  an error/rejection on a *supported, finite* form that Clojure (and the rules)
  accept. Gets fixed; a `GAP-*` later judged intentional is renumbered `DIV-*`.
- **DIV** — an intentional, rule-justified difference, kept as a `DIV-*` entry:
  signal values where Clojure raises on recoverable *data* (rules 1, 4); eager-only
  evaluation / no lazy or infinite seqs (rule 2); the Java type model (rule 3); and
  PTC's value-model, determinism, and no-macros/`eval` choices.
- **UNSUPPORTED** — a Clojure feature PTC has not implemented (an audit
  *candidate*, e.g. `clojure.string/capitalize`); the conformance runner skips it
  rather than running it wrong.
- **UNKNOWN** — observed, not yet triaged.

Correctly raising on a genuine *program* fault (`(+ 1 nil)`, wrong arity) is not a
divergence at all — it is the intended behavior (rule 4).

### Non-Goals

- General-purpose programming
- Full Clojure compatibility

### Clojure Extensions

PTC-Lisp extends standard Clojure with features designed for data transformation in agentic contexts. These are **not valid Clojure** but provide significant utility for LLM-generated programs:

| Extension | Description |
|-----------|-------------|
| Implicit `do` | Multiple expressions in `fn`, `let`, `when`, `when-let` bodies (§5, §13.1) |
| `data/path`, `tool/name` | Namespace-qualified access to context data and tool invocation (§9) |
| `*1`, `*2`, `*3` | Turn history symbols for accessing previous results (§9.4) |
| `sum`, `avg` | Simple collection aggregators (§8) |
| `sum-by`, `avg-by`, `min-by`, `max-by`, `distinct-by` | Field-based collection aggregators (§8) |
| `min-key`, `max-key` | Clojure-compatible variadic key comparison (§8) |
| `re-pattern`, `#"..."` | Compile string to regex (§8.10) |
| `floor`, `ceil`, `round`, `trunc` | Integer rounding |
| `float`, `double`, `int` | Type coercion (to float / to integer) |
| `call` | Tool invocation special form (§9) |
| `return`, `fail` | Control flow for multi-turn agentic loops (§5.22, §5.23) |
| `doseq` | Side-effecting iteration (§5.21) |

All other syntax and functions are valid Clojure and are tested against Babashka for conformance.

---

## 2. Lexical Structure

### 2.1 Whitespace

Whitespace separates tokens. The following are whitespace:
- Space (` `)
- Tab (`\t`)
- Newline (`\n`, `\r\n`)
- Comma (`,`) — treated as whitespace for readability

```clojure
{:a 1, :b 2}    ; comma is optional
{:a 1 :b 2}    ; equivalent
[1, 2, 3]      ; comma is optional
[1 2 3]        ; equivalent
```

### 2.2 Comments

Single-line comments start with `;` and extend to end of line:

```clojure
; This is a comment
(+ 1 2) ; inline comment
```

### 2.3 Identifiers (Symbols)

Symbols are names that refer to values or functions:

```
symbol        = symbol-first symbol-rest*
symbol-first  = letter | special-initial
symbol-rest   = letter | digit | special-rest
letter        = a-z | A-Z
digit         = 0-9
special-initial = + | - | * | / | < | > | = | ? | ! | _ | % | . | &
special-rest    = special-initial | '
```

Notes:
- `/` appears in both `special-initial` (for the division operator) and `special-rest` (for namespaced symbols like `data/bar` or `tool/search`)
- `.` enables Clojure-style multi-level namespaces (e.g., `clojure.string/join`)
- `%` supports parameter placeholders in `#()` short function syntax (`%1`, `%&`, etc.)
- `&` supports rest parameter destructuring (`[a & rest]`)
- `_` is allowed (e.g., ignored bindings)
- `'` is allowed only after the first character, for Clojure prime-notation names like `inc'`, `+'`

Valid symbols: `filter`, `map`, `sort-by`, `empty?`, `+`, `->>`, `high-paid`, `data/bar`, `tool/search`, `clojure.string/join`

Reserved symbols (cannot be redefined): `nil`, `true`, `false`

### 2.4 Keywords

Keywords are symbolic identifiers that evaluate to themselves:

```
keyword      = ":" keyword-char+
keyword-char = letter | digit | + | - | * | < | > | = | ? | ! | _
```

Keywords use a stricter character set than symbols — `/`, `.`, `%`, and `&` are **not** allowed (this is why namespaced keywords are unsupported).

Examples: `:name`, `:user-id`, `:total`, `:else`

Keywords with namespaces are **not supported**: ~~`:foo/bar`~~ (see [DIV-13](clojure-conformance-gaps.md#div-13-namespaced-keywords-not-supported))

---

## 3. Data Types

### 3.1 Nil

The absence of a value:

```clojure
nil
```

### 3.2 Booleans

```clojure
true
false
```

### 3.3 Numbers

**Integers** — arbitrary-precision arithmetic (literals are limited to 100 digits):
```clojure
0
42
-17
1000000
```

**Floats** — double precision:
```clojure
3.14
-0.5
1.0
2.5e10
1.23e-4
```

**Special Values (IEEE 754)** — literals and namespaced constants:

| Literal | Constant | Description |
|---------|----------|-------------|
| `##Inf` | `Double/POSITIVE_INFINITY` | Positive infinity |
| `##-Inf` | `Double/NEGATIVE_INFINITY` | Negative infinity |
| `##NaN` | `Double/NaN` | Not a Number |

Special values are returned by operations like division by zero (`(/ 1.0 0.0)`) or indeterminate forms (`(/ 0.0 0.0)`). They are formatted using Clojure's reader syntax (`##Inf`, `##-Inf`, `##NaN`) and are supported as both input literals and output format.

**Not supported:** Ratios (`1/3`), BigDecimals (`1.0M`), octal/hex literals

### 3.4 Strings

Double-quoted, with escape sequences:

```clojure
"hello"
"hello world"
""
"line1\nline2"
"tab\there"
"quote: \""
"backslash: \\"
```

Supported escapes: `\\`, `\"`, `\n`, `\t`, `\r`

**Multi-line strings:** Strings may contain literal newline characters (like Clojure). Escape sequences (`\n`, `\r`) also work.

**Regex literals:** `#"..."` is shorthand for `(re-pattern "...")`. Both forms produce compiled regex values.

**String operations:** Strings support `count`, `empty?`, `seq`, `str`, `pr-str`, `subs`, `join`, `split`, `trim`, `replace`, `index-of`, `last-index-of`, `format`, `name`, `re-find`, and `re-matches`. The `seq` function converts a string to a sequence of characters (graphemes), enabling character iteration. See Section 8.3 and 8.8 for details.

**String as sequence:** Strings can be used as sequences in many collection operations. Functions like `filter`, `map`, `first`, `last`, `take`, `drop`, `reverse`, `sort`, and others work directly on strings, treating them as sequences of characters (graphemes). These operations return lists of single-character strings:

```clojure
(first "hello")                    ; => "h"
(filter #(= \e %) "hello")         ; => ["e"]
(map identity "abc")               ; => ["a" "b" "c"]
(take 2 "hello")                   ; => ["h" "e"]
(count (filter #(= \r %) "raspberry"))  ; => 3
```

### 3.5 Character Literals

Character literals provide a concise syntax for single-character strings, using Clojure's backslash notation:

```clojure
\a          ; => "a"
\Z          ; => "Z"
\5          ; => "5"
\λ          ; => "λ" (Unicode supported)
```

**Special characters** use named escapes:

| Literal | Value | Description |
|---------|-------|-------------|
| `\newline` | `"\n"` | Newline |
| `\space` | `" "` | Space |
| `\tab` | `"\t"` | Tab |
| `\return` | `"\r"` | Carriage return |
| `\backspace` | `"\b"` | Backspace |
| `\formfeed` | `"\f"` | Form feed |

**Important:** Character literals are represented as single-character strings internally. This means `\r` produces the string `"r"`, while `\return` produces `"\r"` (carriage return). Character equality with strings works naturally:

```clojure
(= \a "a")           ; => true
(= \newline "\n")    ; => true
(char? \a)           ; => true
(char? "ab")         ; => false
```

**Use case:** Character literals are particularly useful with collection operations on strings:

```clojure
;; Count occurrences of 'r' in a string
(count (filter #(= \r %) "raspberry"))  ; => 3

;; Find vowels
(filter #(contains? #{\a \e \i \o \u} %) "hello")  ; => ["e" "o"]
```

### 3.6 Keywords

Self-evaluating symbolic identifiers:

```clojure
:name
:user-id
:category
:else
```

Keywords can be called as functions to access map values:

```clojure
(:name {:name "Alice" :age 30})  ; => "Alice"
(:missing {:name "Alice"})       ; => nil
(:missing {:name "Alice"} "default")  ; => "default"
```

Maps can also be called as functions with a keyword to access values:

```clojure
({:name "Alice" :age 30} :name)  ; => "Alice"
({:name "Alice"} :missing)       ; => nil
({:name "Alice"} :missing "default")  ; => "default"
```

Keywords also work as predicates in higher-order functions, checking if the field is truthy:

```clojure
;; As predicate in filter/remove/find (checks field truthiness)
(filter :active [{:active true} {:active false}])  ; => [{:active true}]
(remove :deleted [{:deleted true} {:deleted nil}]) ; => [{:deleted nil}]

;; As accessor in map (extracts field value)
(map :name [{:name "Alice"} {:name "Bob"}])        ; => ["Alice" "Bob"]
```

### 3.7 Vectors

Ordered, indexed collections:

```clojure
[]
[1 2 3]
["a" "b" "c"]
[1 "mixed" :types true nil]
[[1 2] [3 4]]  ; nested
```

### 3.8 Maps

Key-value associations:

```clojure
{}
{:name "Alice"}
{:name "Alice" :age 30}
{:user {:name "Bob" :email "bob@example.com"}}  ; nested
{"string-key" 42}  ; string keys allowed
```

**Map keys:** Keywords and strings are the standard map key types — keywords are preferred for their readability and self-documenting nature. Other key types (numbers, vectors) evaluate without error inside a program, but you should not rely on them for outputs because the serialization boundaries treat them inconsistently:

- **`return` value:** maps with non-string/non-keyword keys are **rejected** (the program errors).
- **Tool-call arguments:** all keys are **stringified** (e.g. `1` → `"1"`, `[:a :b]` → its inspected form), never rejected.
- **`json/generate-string`:** integer keys are stringified (`{1 "a"}` → `{"1":"a"}`), but vector/float keys (and even keyword keys) return `nil`.

```clojure
{:name "Alice"}           ; OK - keyword key
{"name" "Alice"}          ; OK - string key
{1 "one"}                 ; evaluates fine; serialization depends on the boundary (see above)
{[:a :b] "nested"}        ; evaluates fine; rejected by `return`, stringified for tool args
```

For predictable behavior, use keyword or string keys for any map you intend to return or pass to a tool.

**Maps as functions:** Maps can be invoked as functions to look up values by key:

| Expression | Result | Description |
|------------|--------|-------------|
| `({:a 1 :b 2} :a)` | `1` | Keyword key lookup |
| `({:a 1} :missing)` | `nil` | Missing key returns nil |
| `({:a 1} :missing "default")` | `"default"` | Missing key with default |
| `({"name" "Alice"} "name")` | `"Alice"` | String key lookup |

**Note:** Maps cannot be passed directly to higher-order functions like `mapv` or `filter`. Use a wrapper closure instead:

```clojure
;; Won't work: (mapv my-map keys)
;; Use instead:
(let [lookup {:a 1 :b 2}]
  (mapv #(lookup %) [:a :b]))  ; => [1 2]
```

### 3.9 Sets

Unordered collections of unique values:

```clojure
#{}                    ; empty set
#{1 2 3}               ; set with 3 elements
#{1 1 2}               ; duplicates silently removed: equivalent to #{1 2}
#{:a :b :c}            ; keyword set
```

Sets are **unordered** - iteration order is not guaranteed.

**Set operations:**

| Function | Signature | Description |
|----------|-----------|-------------|
| `set?` | `(set? x)` | Returns true if x is a set |
| `set` | `(set coll)` | Convert collection to set |
| `vec` | `(vec coll)` | Convert collection to vector |
| `vector` | `(vector & args)` | Create vector from arguments |
| `count` | `(count #{1 2})` | Returns element count |
| `empty?` | `(empty? #{})` | Returns true if empty |
| `contains?` | `(contains? #{1 2} 1)` | Membership test (O(1)) |
| `intersection` | `(clojure.set/intersection & sets)` | Returns the intersection of one or more sets |
| `union` | `(clojure.set/union & sets)` | Returns the union of zero or more sets |
| `difference` | `(clojure.set/difference & sets)` | Returns the difference of one or more sets |

**Sets as predicates:** Sets can be invoked as functions to check membership:

| Expression | Result | Description |
|------------|--------|-------------|
| `(#{1 2 3} 2)` | `2` | Element found, returns it |
| `(#{1 2 3} 4)` | `nil` | Not found, returns nil |
| `(filter #{:a :b} [:a :c :b])` | `[:a :b]` | Filter using set membership |
| `(some #{"x"} ["a" "x"])` | `"x"` | Find first matching element |

**Not supported for sets:** `first`, `last`, `nth`, `sort` (sets are unordered). Note that `sort-by` *is* supported on sets — it iterates the elements and returns a sorted vector.

**Not supported:** Lists (`'()`)

### 3.10 Vars

Vars are references to bindings created by the `def` form. They allow you to create references to named values that can be stored in collections and passed around.

**Reader syntax:** The `#'name` syntax produces a var reference:

```clojure
#'x                    ; var reference to binding x
#'my-var               ; var reference to binding my-var
#'suspicious?          ; var reference to binding suspicious?
#'save!                ; var reference to binding save!
```

Vars can be stored in collections:

| Expression | Description |
|------------|-------------|
| `[#'x #'y]` | Vector containing two var references |
| `{:result #'foo}` | Map with var reference as value |
| `#{#'a #'b #'c}` | Set containing var references |

**Var dereferencing:** The actual dereferencing of vars and access to the values they reference is handled by the `def` form. See the `def` form documentation for details on how var bindings work and how vars are evaluated.

---

## 4. Truthiness

Only `nil` and `false` are **falsy**. Everything else is **truthy**:

| Value | Truthy? |
|-------|---------|
| `nil` | No |
| `false` | No |
| `true` | Yes |
| `0` | Yes |
| `""` (empty string) | Yes |
| `[]` (empty vector) | Yes |
| `{}` (empty map) | Yes |
| Any other value | Yes |

```clojure
(if nil "truthy" "falsy")    ; => "falsy"
(if false "truthy" "falsy")  ; => "falsy"
(if true "truthy" "falsy")   ; => "truthy"
(if 0 "truthy" "falsy")      ; => "truthy"
(if "" "truthy" "falsy")     ; => "truthy"
(if [] "truthy" "falsy")     ; => "truthy"
(if {} "truthy" "falsy")     ; => "truthy"
```

---

## 5. Special Forms

Special forms are fundamental constructs with special evaluation rules.

### 5.1 `let` — Local Bindings

Binds names to values for use in the body expression:

```clojure
(let [name value]
  body)

(let [name1 value1
      name2 value2]
  body)
```

**Semantics:**
- Bindings are evaluated left-to-right
- Later bindings can reference earlier ones
- Bindings are scoped to the body
- Inner `let` can shadow outer bindings

```clojure
(let [x 10] x)                    ; => 10
(let [x 10] (+ x 5))              ; => 15
(let [x 1 y 2] (+ x y))           ; => 3
(let [x 1 y (+ x 1)] y)           ; => 2
```

```clojure
(let [x 10
      y (+ x 5)]    ; y can use x
  (* x y))          ; => 150

(let [x 1]
  (let [x 2]        ; shadows outer x
    x))             ; => 2
```

#### Implicit `do` (Clojure Extension)

Multiple body expressions are supported without explicit `do`:

```clojure
;; Multiple expressions - last value is returned
(let [x 10]
  (def saved x)     ; side effect: store in memory
  (* x 2))          ; => 20, saved = 10

;; Equivalent to explicit do
(let [x 10]
  (do
    (def saved x)
    (* x 2)))
```

#### Destructuring
Destructuring allows you to bind names to values within collections.

**Sequential (Vector) Destructuring:**
Extract values from vectors by position.

```clojure
; Basic sequential destructuring
(let [[a b] [1 2]]
  (+ a b))  ; => 3

; Use _ to skip elements
(let [[_ b] [1 2]]
  b)        ; => 2

; Nested sequential destructuring
(let [[a [b c]] [1 [2 3]]]
  (+ a b c)) ; => 6

; Rest pattern: bind remaining elements to a variable
(let [[x & rest] [1 2 3 4]]
  rest)      ; => [2 3 4]

; Rest pattern with multiple leading elements
(let [[a b & rest] [1 2 3 4 5]]
  [a b rest]) ; => [1 2 [3 4 5]]

; Bind entire list (no leading elements)
(let [[& all] [1 2 3]]
  all)       ; => [1 2 3]
```

**Map Destructuring:**
Extract values from maps by key. Supports both keyword and string keys.

```clojure
; Basic map destructuring
(let [{:keys [name age]} {:name "Alice" :age 30}]
  name)  ; => "Alice"

; With defaults
(let [{:keys [name age] :or {age 0}} {:name "Bob"}]
  age)   ; => 0

; Renaming bindings
(let [{the-name :name} {:name "Carol"}]
  the-name)  ; => "Carol"

; Binding the whole map with :as
(let [{:keys [id] :as user} {:id 123 :name "Alice"}]
  (:name user)) ; => "Alice"

; String key destructuring (useful for JSON-like data)
(let [{:strs [name age]} {"name" "Alice" "age" 30}]
  name)  ; => "Alice"
```

**Supported destructuring forms:**
- `[a b]` — sequential (vector)
- `[a & rest]` — rest pattern (bind remaining elements)
- `{:keys [a b]}` — map keyword keys
- `{:strs [a b]}` — map string keys
- `{:keys [a] :or {a default}}` — map with defaults
- `{new-name :old-key}` — map renaming
- `{:as symbol}` — bind collection to symbol


### 5.2 `if` — Conditional

Conditional (else is **optional**):

```clojure
(if condition
  then-expression
  else-expression)
```

```clojure
(if true "yes" "no")              ; => "yes"
(if false "yes" "no")             ; => "no"
(if (> 5 3) "bigger" "smaller")   ; => "bigger"
(if (< 5 3) "bigger" "smaller")   ; => "smaller"
(if (empty? []) "empty" "full")   ; => "empty"
(if (empty? [1]) "empty" "full")  ; => "full"
```

**Single-branch `if` is allowed** and returns `nil` if the condition is false. However, `when` is often more idiomatic for side effects.

### 5.3 `if-not` — Negative Conditional

Swapped branch conditional. Evaluates `else` if condition is truthy, otherwise evaluates `then`.

```clojure
(if-not condition
  then-expression
  else-expression?)
```

**Semantics:**
- Desugars at analysis time to `if`:
  - `(if-not cond then else)` → `(if cond else then)`
  - `(if-not cond then)` → `(if cond nil then)`

```clojure
(if-not true "yes" "no")           ; => "no"
(if-not false "yes" "no")          ; => "yes"
(if-not (> 3 5) "smaller" "bigger") ; => "smaller"
(if-not true "yes")                ; => nil
(if-not false "yes")               ; => "yes"
```

### 5.4 `when` — Single-branch Conditional

Returns body if condition is truthy, otherwise `nil`:

```clojure
(when condition
  body)
```

```clojure
(when true "yes")                 ; => "yes"
(when false "yes")                ; => nil
(when (> 5 3) "bigger")           ; => "bigger"
(when (< 5 3) "smaller")          ; => nil
```

**Implicit `do` (Clojure Extension):** Multiple body expressions are supported:

```clojure
(when (> x 0)
  (def positive x)    ; side effect
  (* x 2))            ; return value
```

### 5.5 `when-not` — Negative Single-branch Conditional

Returns body if condition is falsy, otherwise `nil`:

```clojure
(when-not condition
  body)
```

**Semantics:**
- Desugars at analysis time to `if`: `(when-not cond body ...)` → `(if cond nil (do body ...))`
- Supports implicit `do` for multiple body expressions.

```clojure
(when-not false "yes")            ; => "yes"
(when-not true "yes")             ; => nil
(when-not (> x 0) (log "neg"))    ; => result of log, or nil
```

### 5.6 `cond` — Multi-way Conditional

Tests conditions in order, returns first matching result:

```clojure
(cond
  condition1 result1
  condition2 result2
  :else default-result)
```

```clojure
(cond
  (> total 1000) "high"
  (> total 100)  "medium"
  :else          "low")
```

**Semantics:**
- Conditions are evaluated in order
- First truthy condition's result is returned
- `:else` is conventional for default (it's truthy)
- Returns `nil` if no condition matches and no `:else`

```clojure
(cond true "first" :else "default")           ; => "first"
(cond false "first" :else "default")          ; => "default"
(cond false "a" false "b" :else "c")          ; => "c"
(cond (> 5 3) "yes" :else "no")               ; => "yes"
(cond (< 5 3) "yes" :else "no")               ; => "no"
(cond false "only")                           ; => nil
```

### 5.7 `case` — Value Dispatch

Dispatches on an expression's value against compile-time constants:

```clojure
(case expr
  value1 result1
  value2 result2
  (:val3 :val4) result3    ; grouped match
  default-result)           ; optional trailing default
```

- Test values must be compile-time constants (keywords, strings, numbers, booleans, nil)
- Grouped values `(:val1 :val2)` match any value in the group
- Returns `nil` if no match and no default (diverges from Clojure which throws)
- Expression evaluated exactly once

```clojure
(case :a :a 1 :b 2)                  ; => 1
(case :z :a 1 :b 2 99)               ; => 99
(case :c (:a :b) 1 (:c :d) 2)        ; => 2
(case :z :a 1)                        ; => nil
(case nil nil "matched" :a "nope")    ; => "matched"
```

### 5.8 `condp` — Predicate Dispatch

Dispatches using a predicate function called as `(pred test-val expr)`:

```clojure
(condp pred expr
  test1 result1
  test2 result2
  default-result)           ; optional trailing default
```

- Calls `(pred test-val expr)` for each clause
- Both `pred` and `expr` evaluated exactly once
- Returns `nil` if no match and no default (diverges from Clojure which throws)
- The `:>>` form is not supported

```clojure
(condp = :a :a 1 :b 2)               ; => 1
(condp > 5 10 "big" 3 "small")       ; => "big" (because (> 10 5) is true)
(condp = :z :a 1 "default")          ; => "default"
(condp = :z :a 1 :b 2)               ; => nil
```

### 5.9 `if-let` and `when-let` — Conditional Binding

Binds a value from an expression and evaluates the body only if the value is truthy.

**`if-let` syntax:**
```clojure
(if-let [name condition-expr]
  then-expr
  else-expr)
```

**`when-let` syntax:**
```clojure
(when-let [name condition-expr]
  body-expr)
```

**Semantics:**
- `if-let` evaluates `condition-expr`, binds result to `name`, then evaluates `then-expr` if truthy, otherwise `else-expr`
- `when-let` is like `if-let` but returns `nil` instead of an else branch
- Both only support single symbol bindings, no destructuring (see [DIV-14](clojure-conformance-gaps.md#div-14-if-let-when-let-only-support-single-symbol-bindings))
- Desugars at analysis time: `(if-let [x expr] then else)` → `(let [x expr] (if x then else))`

**Examples:**
```clojure
(if-let [user (get-user 123)]
  (str "Hello " user)
  "User not found")               ; => ...

(when-let [result (compute)]
  (process result))               ; => result of process, or nil

(if-let [x 0]
  "truthy"
  "falsy")                        ; => "truthy" (0 is truthy in Lisp)

(if-let [x nil]
  "yes"
  "no")                           ; => "no"

(when-let [x false]
  (+ x 1))                       ; => nil (x is falsy, body not evaluated)
```

**Implicit `do` (Clojure Extension):** `when-let` supports multiple body expressions:

```clojure
(when-let [x (find-value)]
  (def found x)     ; side effect
  (* x 2))          ; return value
```

**Limitations:**
- Only single bindings are supported (no sequential bindings like Clojure)
- Binding names must be symbols (no destructuring patterns)

---

### 5.10 `if-some` and `when-some` — Nil-safe Conditional Binding

Like `if-let`/`when-let` but tests only for `nil`, not falsiness. `false` binds successfully.

**`if-some` syntax:**
```clojure
(if-some [name expr]
  then-expr
  else-expr)
```

**`when-some` syntax:**
```clojure
(when-some [name expr]
  body-expr ...)
```

**Semantics:**
- `if-some` evaluates `expr`, binds result to `name`, then evaluates `then-expr` if the value is not `nil`, otherwise `else-expr`
- `when-some` returns `nil` when the value is `nil`, otherwise evaluates the body
- The key difference from `if-let`/`when-let`: `false` is treated as a valid (non-nil) value
- Desugars at analysis time: `(if-some [x expr] then else)` → `(let [x expr] (if (nil? x) else then))`

**Examples:**
```clojure
(if-some [x 42] x :nope)            ; => 42
(if-some [x nil] x :nope)           ; => :nope
(if-some [x false] x :nope)         ; => false (false is NOT nil)

(when-some [x 42] (inc x))          ; => 43
(when-some [x nil] (inc x))         ; => nil
(when-some [x false] x)             ; => false
```

**Implicit `do`:** `when-some` supports multiple body expressions:

```clojure
(when-some [x (find-value)]
  (def found x)
  (* x 2))
```

---

### 5.11 `when-first` — First Element Binding

Binds the first element of a collection and evaluates the body only if the collection is non-empty.

**Syntax:**
```clojure
(when-first [name coll-expr]
  body-expr ...)
```

**Semantics:**
- Evaluates `coll-expr` once, calls `seq` on it
- If the result is `nil` (empty or nil collection), returns `nil`
- Otherwise, binds the first element to `name` and evaluates the body
- Single-evaluation: the collection expression is only evaluated once

**Examples:**
```clojure
(when-first [x [1 2 3]] x)          ; => 1
(when-first [x []] x)               ; => nil
(when-first [x nil] x)              ; => nil

(when-first [x [10]]
  (def a x)
  (* a 2))                          ; => 20
```

---

### 5.12 `do` — Sequential Evaluation

Evaluates expressions in order, returning the value of the last expression:

```clojure
(do expr1 expr2 ... exprN)
```

**Semantics:**
- All expressions are evaluated left-to-right
- The value of the last expression is returned
- `(do)` with no expressions returns `nil`
- Unlike `and`/`or`, there is no short-circuiting

```clojure
1 2 3                             ; => 3 (not needed at top level)
(tool/log {:msg "hi"})            ; => result of log call
(do)                              ; => nil
```

---

### 5.13 `def` — User Namespace Binding

Binds a name to a value in the user namespace, persisting across turns:

```clojure
(def name value)
(def name docstring value)  ; docstring is optional; preserved in function metadata, otherwise ignored
```

**Semantics:**
- Returns the var (`#'name`), not the value (like Clojure)
- Creates or overwrites the binding in user namespace
- Value is evaluated before binding
- Binding persists until session ends or redefined
- Shadows builtin names — the user binding takes precedence until session ends
- Can shadow data names, but `data/` prefix still works

```clojure
(def x 42)                        ; => #'x (x = 42)
(def threshold 5000)              ; => #'threshold
(def results (tool/search {}))    ; => ...

; Redefinition
(def x 1)                         ; x = 1
(def x 2)                         ; x = 2 (overwrites)

; Define and return (using implicit multi-expression)
(def x 10) x                      ; => 10

; Reference previous defs (single evaluation)
(def a 1) (def b (+ a 1)) b       ; => 2

; Shadowing builtins (like Clojure — user binding takes precedence)
(def map {})                      ; => #'map (builtin map no longer accessible)
```

**Differences from Clojure:**
- No `^:dynamic`, `^:private`, or other metadata
- No destructuring in def (use `let` then `def`)
- Docstrings allowed; preserved in function metadata (via `defn`), otherwise ignored

---

### 5.14 `defonce` — Idempotent Initialization

Binds a name to a value only if not already defined. Safe for multi-turn use:

```clojure
(defonce name value)
(defonce name docstring value)
```

**Semantics:**
- If name is already bound in user namespace: no-op, returns the var
- If name is not bound: evaluates value and binds it (same as `def`)
- Value expression is NOT evaluated if name is already bound
- Shadows builtin names (same as `def`)

```clojure
(defonce total-episodes 0)            ; turn 1 → binds 0, turn 2+ → no-op
(def total-episodes (inc total-episodes))  ; safe to use after defonce
```

---

### 5.15 `defn` — Named Function Definition

Syntactic sugar for defining named functions in the user namespace:

```clojure
(defn name [params] body)
(defn name docstring [params] body)  ; docstring is optional and ignored
```

**Desugars to:** `(def name (fn [params] body))`

**Semantics:**
- Returns the var (`#'name`), not the function
- Creates or overwrites the function binding in user namespace
- Functions persist across turns via user namespace
- Can reference other user-defined symbols and functions
- Can access `data/` data and call `tool/` tools
- Shadows builtin names (same as `def`)

```clojure
; Note: using `twice` not `double` since `double` is a builtin (§8.4)
(defn twice [x] (* x 2))              ; => #'twice
(defn greet [name] (str "Hello, " name))  ; => #'greet

; Use defined function (single evaluation with implicit do)
(defn twice [x] (* x 2)) (twice 21)   ; => 42

; Reference data/ data
(defn expensive? [e] (> (:amount e) data/threshold))

; Reference other defs (single evaluation)
(def rate 0.1) (defn apply-rate [x] (* x rate)) (apply-rate 100)  ; => 10.0

; With higher-order functions
(defn expensive? [e] (> (:amount e) 5000))
(filter expensive? data/expenses)  ; => filtered list
```

**Multiple body expressions (implicit do):**
```clojure
(defn with-logging [x]
  (def last-input x)                  ; side effect
  (* x 2))                            ; return value
```

**Multi-turn persistence:**
```clojure
; Turn 1: Define function
(defn expensive? [e] (> (:amount e) 5000))

; Turn 2: Use function (passed via memory)
(filter expensive? data/expenses)
```

**Destructuring in parameters:**
`defn` supports the same destructuring patterns as `fn` and `let`:

```clojure
; Vector destructuring (single evaluation)
(defn first-name [[first last]] first) (first-name ["Alice" "Smith"])  ; => "Alice"

; Map destructuring (single evaluation)
(defn greet [{:keys [name]}] (str "Hello " name)) (greet {:name "World"})  ; => "Hello World"

; Nested destructuring (single evaluation)
(defn process [[id {:keys [status]}]] (str id ":" status)) (process [42 {:status "ok"}])  ; => "42:ok"
```


**Not supported:** Multi-arity `defn` ([DIV-15](clojure-conformance-gaps.md#div-15-no-multi-arity-defn)), pre/post conditions ([DIV-16](clojure-conformance-gaps.md#div-16-no-pre-post-conditions-in-defn)).

---

### 5.16 `loop` and `recur` — Tail Recursion

`loop` establishes a recursion point, and `recur` transfers control back to that point with new values.

**`loop` syntax:**
```clojure
(loop [bindings] body)
```

**`recur` syntax:**
```clojure
(recur expr1 expr2 ...)
```

**Semantics:**
- `loop` establishes bindings just like `let`.
- `recur` can only appear in a **tail position** of a `loop` or `fn`.
- When `recur` is evaluated, it re-binds the arguments and jumps back to the start of the `loop` or `fn` body.
- Evaluation is **stack-safe** (no stack growth).
- An iteration check is enforced to prevent infinite loops (default limit: 1000 iterations).

**Examples:**
```clojure
;; Summing numbers 0 to 4
(loop [i 0 acc 0]
  (if (< i 5)
    (recur (inc i) (+ acc i))
    acc))
; => 10

;; Factorial with recur in fn
((fn [n acc]
   (if (> n 0)
     (recur (dec n) (* acc n))
     acc))
 5 1)
; => 120

;; Process list with rest pattern destructuring
(loop [[head & tail] [1 2 3 4]
       sum 0]
  (if head
    (recur tail (+ sum head))
    sum))
; => 10
```

**Safety Mechanism:**
PTC-Lisp enforces an iteration limit on `loop`/`recur` jumps. If a `loop` or tail-recursive function using `recur` exceeds the allowed number of iterations (default 1000), execution is terminated with a `loop_limit_exceeded` error. Ordinary non-tail function recursion is not counted by this limit; it remains bounded by the sandbox timeout and memory limit.

### 5.17 `for` — List Comprehension

`for` produces a vector by evaluating a body expression for each element of one or more collections. (Unlike Clojure's lazy sequence, PTC-Lisp's `for` returns an eager vector, displayed as `[...]`.)

**Single binding:**

```clojure
(for [x [1 2 3]] (* x 2))
; => [2 4 6]
```

**Multiple bindings (cartesian product):**

```clojure
(for [x [1 2] y ["a" "b"]] [x y])
; => [[1 "a"] [1 "b"] [2 "a"] [2 "b"]]
```

**Destructuring:**

```clojure
(for [[k v] {:a 1 :b 2}] (str k "=" v))
; => [":a=1" ":b=2"]  (order may vary)
```

**Multi-expression body** (implicit `do`, last value collected):

```clojure
(for [x [1 2 3]]
  (println x)
  (* x 10))
; prints 1, 2, 3
; => [10 20 30]
```

**Comparison with `map`:** `for` supports multiple bindings (cartesian product) and destructuring in bindings. For simple single-collection transforms, `map` is equivalent:

```clojure
(map inc [1 2 3])           ; => [2 3 4]
(for [x [1 2 3]] (inc x))  ; => [2 3 4]
```

**Modifiers:** `:when`, `:let`, and `:while` follow a binding pair and modify iteration:

```clojure
;; :when — filter elements (skip on false, continue iterating)
(for [x [1 2 3 4 5] :when (odd? x)] x)
; => [1 3 5]

;; :let — introduce local bindings
(for [x [1 2 3] :let [y (* x 10)]] y)
; => [10 20 30]

;; :while — stop iterating at this level when false
(for [x [1 2 3 4 5] :while (< x 4)] x)
; => [1 2 3]

;; Combined: modifiers apply in declaration order
(for [x [1 2 3 4] :when (odd? x) :let [y (* x 10)]] y)
; => [10 30]

;; :let visible to subsequent :when
(for [x [1 2 3] :let [y (* x 2)] :when (> y 3)] y)
; => [4 6]

;; :while on inner binding only stops inner loop
(for [x [1 2] y [10 20 30] :while (< y 25)] [x y])
; => [[1 10] [1 20] [2 10] [2 20]]
```

Multiple `:when` clauses act as AND (all must pass). `:let` supports destructuring.

### 5.18 `task` — Journaled Task Execution

> **Advanced.** Most agents use `plan:` labels for progress visibility and let the application own idempotency. These forms are for explicit crash-safe caching when the application needs resumability across re-invocations. Requires `journaling: true`.

`task` executes an expression with caching and idempotency semantics. When a journal is available, tasks are memoized by ID, enabling safe retry loops in agentic execution.

**Syntax:**

```clojure
(task "unique-id" expr)
```

**Semantics:**

- **With journal:** Expression result is cached by ID on first execution. Subsequent calls with the same ID return the cached result without re-executing.
- **Without journal:** Expression executes normally with no caching. A debug-level log message is emitted to note that caching and idempotency are inactive.
- **Failure handling:** If `expr` raises an error or calls `(fail)`, the result is not cached and the error propagates.

**Examples:**

```clojure
;; Cache expensive operation
(task "fetch-users" (tool/get-users {:limit 1000}))
; First call: executes tool, caches result
; Second call: returns cached result

;; Use with conditional execution
(let [users (task "fetch-users" (tool/get-users {}))]
  (count users))

;; Multiple tasks in sequence
(do
  (def step1 (task "step1" (tool/process {:data data/input})))
  (def step2 (task "step2" (tool/analyze step1)))
  step2)
```

**Use Cases:**

- **Safe retries:** Cache intermediate results during multi-turn agentic loops, ensuring tool calls aren't repeated
- **Deterministic replay:** Re-run a program with the same journal to get identical results
- **Resource optimization:** Avoid redundant API calls or expensive computations

**Important Notes:**

- Task IDs are typically **string literals** for predictable caching; expressions that evaluate to strings are also accepted (and coerced via `to_string`)
- IDs should be **semantically meaningful** to enable consistent caching across retries (e.g., `"fetch-users"` rather than `"step1"`)
- The journal is provided at execution time; running without a journal disables caching but still executes the expression
- Task results must be serializable (maps, lists, primitives, etc.)

---

### 5.19 `step-done` — Semantic Progress Reporting

`step-done` records a summary for a plan step, signaling completion with a human-readable description. Used with the `plan` option on SubAgent to render progress checklists.

**Syntax:**

```clojure
(step-done "step-id" "summary of what was accomplished")
```

**Semantics:**

- Stores the summary string in the step's `summaries` map, keyed by ID
- Returns the summary string
- Summaries accumulate across turns within a SubAgent run
- If called multiple times with the same ID, the last summary wins
- **Deferred visibility:** summaries appear in the Progress checklist on the *next* turn, not the current one. If the current turn errors, its summaries are discarded.
- **Must be called at top level** — summaries inside `pmap`, `pcalls`, or `map` closures are not propagated (closures run in isolated contexts)

**Examples:**

```clojure
;; Report completion of a plan step
(do
  (def users (task "gather" (tool/get-users {})))
  (step-done "gather" (str "Found " (count users) " users")))

;; Multiple steps
(do
  (step-done "1" "Fetched 42 records from API")
  (step-done "2" "Filtered to 12 matching criteria"))
```

---

### 5.20 `task-reset` — Clear Journaled Task Cache

`task-reset` removes a cached task result from the journal, allowing it to be re-executed on the next call to `(task id expr)`.

**Syntax:**

```clojure
(task-reset "task-id")
```

**Semantics:**

- Deletes the given key from the journal map
- Returns `nil`
- No-op if the key doesn't exist in the journal
- Only affects the journal (task cache), not summaries

**Examples:**

```clojure
;; Re-fetch stale data
(task-reset "fetch-users")
(def users (task "fetch-users" (tool/get-users {:limit 1000})))
```

### 5.21 `doseq` — Side-effecting Iteration

`doseq` iterates over collections for side effects (like `for`, but returns `nil` instead of collecting results). Desugars to `loop`/`recur` at analysis time.

**Syntax:**

```clojure
(doseq [binding coll] body)
(doseq [b1 coll1 b2 coll2] body)   ; nested loops
```

**Semantics:**

- Iterates over each element, executing the body for side effects
- Supports multiple bindings (nested loops, cartesian product)
- Supports destructuring in bindings
- Supports `:when`, `:let`, `:while` modifiers (same as `for`)
- Always returns `nil`

```clojure
(doseq [x [1 2 3]] (println x))
; prints 1, 2, 3
; => nil

(doseq [x [1 2] y ["a" "b"]] (println x y))
; prints "1 a", "1 b", "2 a", "2 b"
; => nil

(doseq [[a b] [[1 2] [3 4]]] (println (+ a b)))
; prints 3, 7
; => nil
```

---

### 5.22 `return` — Signal Successful Completion

`return` immediately terminates execution and returns the given value as the result. Used in multi-turn agentic loops to signal that the agent has completed its task.

**Syntax:**

```clojure
(return value)
```

**Semantics:**

- Immediately terminates the current program execution
- The value becomes the program result, wrapped in a `return_signal`
- Cannot be used inside `pmap` or `pcalls` (raises an error)

```clojure
;; Signal completion in a multi-turn loop
(if (>= (count results) target)
  (return {:status "complete" :results results})
  (tool/fetch-more {}))
```

---

### 5.23 `fail` — Signal Failure

`fail` immediately terminates execution with an error. Used in multi-turn agentic loops to signal that the agent cannot complete the task.

**Syntax:**

```clojure
(fail error)
```

**Semantics:**

- Immediately terminates the current program execution
- The error value becomes the failure reason, wrapped in a `fail_signal`
- Cannot be used inside `pmap` or `pcalls` (raises an error)

```clojure
;; Signal failure when a required condition isn't met
(if (nil? data/input)
  (fail "No input data provided")
  (process data/input))
```

---

## 6. Threading Macros

Threading macros transform nested function calls into linear pipelines.

### 6.1 `->>` — Thread Last

Threads the value as the **last argument** to each form:

```clojure
(->> value
     (fn1 arg1)
     (fn2 arg2)
     (fn3))
```

Equivalent to:
```clojure
(fn3 (fn2 arg2 (fn1 arg1 value)))
```

**Primary use:** Collection pipelines where data is the last argument.

```clojure
(->> [1 2 3] (map inc))                       ; => [2 3 4]
(->> [1 2 3 4] (filter odd?))                 ; => [1 3]
(->> [3 1 2] (sort))                          ; => [1 2 3]
(->> [1 2 3] (map inc) (filter even?))        ; => [2 4]
(->> [1 2 3 4 5] (filter odd?) (take 2))      ; => [1 3]
```

### 6.2 `->` — Thread First

Threads the value as the **first argument** to each form:

```clojure
(-> value
    (fn1 arg1)
    (fn2 arg2))
```

Equivalent to:
```clojure
(fn2 (fn1 value arg1) arg2)
```

**Primary use:** Map transformations where data is the first argument.

```clojure
(-> {:a 1} (assoc :b 2))                      ; => {:a 1 :b 2}
(-> {:a 1 :b 2} (dissoc :b))                  ; => {:a 1}
(-> {:a 1} (assoc :b 2) (assoc :c 3))         ; => {:a 1 :b 2 :c 3}
(-> {:a {:b 1}} (get-in [:a :b]))             ; => 1
(-> {:a 1} (update :a inc))                   ; => {:a 2}
```

### 6.3 `as->` — Named Thread

Binds the threaded value to a name, making it available in any argument position:

```clojure
(as-> expr name
  form1
  form2)
```

The name is rebound at each step to the result of the previous form. With zero forms, returns the expr directly.

```clojure
(as-> 1 x (+ x 1) (* x 2))           ; => 4
(as-> 42 x)                           ; => 42
(as-> [1 2 3] x (count x))           ; => 3
```

### 6.4 `cond->` and `cond->>` — Conditional Threading

Threads through forms only where the corresponding test is true:

```clojure
(cond-> expr
  test1 form1
  test2 form2)
```

- `cond->` threads as the **first argument** (like `->`)
- `cond->>` threads as the **last argument** (like `->>`)
- Requires even number of test/form pairs after the initial expression
- With zero clauses, returns expr unchanged

```clojure
(cond-> 1 true inc false dec)         ; => 2
(cond-> 42 false inc false dec)       ; => 42
(cond-> 42)                           ; => 42
(cond-> 10 true (- 5))               ; => 5 (thread-first)
(cond->> 10 true (- 5))              ; => -5 (thread-last)
```

### 6.5 `some->` and `some->>` — Nil-safe Threading

Threads through forms, short-circuiting to `nil` if any intermediate result is `nil`:

```clojure
(some-> expr form1 form2)
```

- `some->` threads as the **first argument**
- `some->>` threads as the **last argument**
- `false` is NOT nil — threading continues through `false` values
- With no pipeline steps — `(some-> expr)` — returns `expr` directly

```clojure
(some-> 1 inc)                        ; => 2
(some-> nil inc)                      ; => nil (short-circuits)
(some-> {:a nil} (:a) inc)           ; => nil (mid-chain nil)
(some-> false not)                    ; => true (false is not nil)
(some->> [1 2 3] (map inc))          ; => [2 3 4]
(some->> nil (map inc))              ; => nil
```

---

## 7. Filtering Predicates

Filtering operations (`filter`, `remove`, `some`, `every?`, `not-any?`, `not-every?`, `take-while`, `drop-while`) accept any callable as a predicate. The three common shapes are:

| Shape | Example | When to use |
|-------|---------|-------------|
| Keyword accessor | `(filter :active users)` | Truthy check on a single field |
| Anonymous `fn` / `#()` | `(filter (fn [u] (> (:age u) 18)) users)` | Field comparisons, multi-field logic |
| Named function | `(filter even? xs)` | Standard or user-defined predicate |

```clojure
(count (filter :active [{:active 1} {:active nil} {:active false}])) ; => 1
(count (filter (fn [m] (> (:x m) 1)) [{:x 1} {:x 2} {:x 3}]))        ; => 2
(count (filter #(= (:status %) "active") [{:status "active"} {:status "x"}])) ; => 1
```

### 7.1 Combining Predicates with `and` / `or` / `not`

To check several conditions, build the boolean inside a single `fn`:

```clojure
;; All conditions must hold
(filter (fn [u] (and (= (:status u) "active") (>= (:age u) 18))) users)

;; At least one condition must hold
(filter (fn [u] (or (= (:role u) "admin") (= (:role u) "moderator"))) users)

;; Negation
(remove (fn [i] (:deleted i)) items)
```

```clojure
(count (filter (fn [m] (and (= (:a m) 1) (= (:b m) 2)))
               [{:a 1 :b 2} {:a 1 :b 3}]))           ; => 1
(count (filter (fn [m] (or (= (:a m) 1) (= (:a m) 2)))
               [{:a 1} {:a 2} {:a 3}]))              ; => 2
```

### 7.2 Membership Testing

Use `contains?` against a set or list:

```clojure
;; Pick orders whose status is "active" or "pending"
(filter (fn [o] (contains? #{"active" "pending"} (:status o))) orders)
```

For a variable membership set built from the data itself:

```clojure
(let [premium-ids (->> users
                       (filter (fn [u] (= (:tier u) "premium")))
                       (map :id))
      premium-set (set premium-ids)]
  (filter (fn [o] (contains? premium-set (:user-id o))) orders))
```

### 7.3 Nested Field Access in Predicates

Use `get-in` (or sequential `(:b (:a item))`) for nested fields:

```clojure
(filter (fn [u] (= (get-in u [:profile :verified]) true)) users)
(filter (fn [i] (> (get-in i [:user :age]) 18)) items)
```

### 7.4 Nil Handling in Predicates

Ordering comparisons (`>`, `<`, `>=`, `<=`) are recoverable predicates: they return booleans for `nil` and mixed scalar values instead of raising. PTC-Lisp uses the runtime's total term ordering for non-NaN values, which can be surprising for missing fields. Guard explicitly when missing values should be excluded:

```clojure
;; Safe: filter out users without :age first
(filter (fn [u] (and (some? (:age u)) (> (:age u) 18))) users)
```

For equality checks `nil` is well-defined:

```clojure
(filter (fn [m] (= (:field m) nil)) items)   ; explicitly match nil
(filter (fn [m] (some? (:field m))) items)   ; field exists and is not nil
```

### 7.5 Flexible Key Access — String and Atom Keys

Keyword accessors (`(:status m)`, `(get m :status)`, `(get-in m [:a :b])`) and the key-based aggregators (`sort-by`, `sum-by`, `avg-by`, `min-by`, `max-by`, `distinct-by`, `group-by`) support **bidirectional key matching**:

- Atom keys in code (`:status`) match both atom and string keys in data
- String keys in code (`"status"`) match both string and atom keys in data
- When both exist on the same map, the exact key type matching the accessor takes precedence (an atom accessor wins the atom key; a string accessor wins the string key)
- As a final fallback, hyphens in the key name are normalized to underscores and retried (so `:turn-summaries` matches a `:turn_summaries` or `"turn_summaries"` key)

```clojure
;; Atom keys (preferred Elixir style)
(filter (fn [u] (= (:status u) "active")) users)

;; Works with string-keyed data from JSON APIs
(filter (fn [u] (= (:status u) "active")) data)
;; A %{"status" => "active"} entry matches.

;; String key parameter also works (useful for LLM-generated code)
(sort-by "price" products)
(sum-by "amount" expenses)

;; Nested access with mixed key types
(filter (fn [i] (= (get-in i [:user :email]) "alice@example.com")) items)
;; Matches both %{user: %{"email" => ...}} and %{"user" => %{email: ...}}.
```

**How it works:**
1. When looking up a field, the accessor tries the exact key type first.
2. If not found, it falls back to the alternative type (atom ↔ string).
3. If still not found, the key name's hyphens are normalized to underscores and both atom and string forms of the normalized name are tried.
4. When both exist on the same map, the exact key type takes precedence.
5. This applies to nested fields independently at each level.
6. Missing fields at any level still return `nil`.

This eliminates the need to manually convert JSON responses to atom-keyed maps before filtering and gives some resilience against LLM-generated code that uses strings instead of keywords.

---

## 8. Core Functions

> **Complete function list:** See [Function Reference](function-reference.md) for all
> supported functions with signatures, generated from the canonical registry at `priv/functions.exs`.
> This section covers semantics, edge cases, and examples.

### 8.1 Collection Operations

#### Filtering

| Function | Signature | Description |
|----------|-----------|-------------|
| `filter` | `(filter pred coll)` | Keep items where pred is truthy |
| `filterv` | `(filterv pred coll)` | Same as filter (vectors are the default) |
| `remove` | `(remove pred coll)` | Remove items where pred is truthy |
| `keep` | `(keep f coll)` | Non-nil results of (f item). false is kept. |
| `keep-indexed` | `(keep-indexed f coll)` | Non-nil results of (f index item). false is kept. |
| `dedupe` | `(dedupe coll)` | Remove consecutive duplicates |

```clojure
;; Using a keyword directly (concise, checks truthiness)
(filter :active users)
(remove :deleted items)

;; Using an anonymous fn for richer comparisons
(filter (fn [u] (> (:age u) 18)) users)
;; "First match" is (first (filter ...)) — find is map/vector lookup, not search
(first (filter (fn [u] (= (:id u) 42)) users))
```

`find` is **not** a predicate search: it is associative lookup. `(find coll
key)` returns the `[key value]` entry for `key` in a map, or the `[index
value]` entry for a non-negative integer index in a vector, or `nil` when
absent — mirroring Clojure. It distinguishes a present `nil` value from a
missing key: `(find {:a nil} :a)` => `[:a nil]` while `(find {:a 1} :b)` =>
`nil`. Non-associative inputs (sets, strings) return a `:type_error` signal
(DIV-48).

```clojure
(find {:a 1 :b 2} :b) ;=> [:b 2]
(find {:a 1} :z)      ;=> nil
(find [10 20 30] 2)   ;=> [2 30]
(find [10 20] 5)      ;=> nil
```

**Map support:** `filter` and `remove` accept maps as input, treating each entry as a `[key value]` pair passed to the predicate. They return a **list** of `[key value]` pairs (not a map):

```clojure
;; Filter map entries by value
(filter (fn [[k v]] (> v 100)) {:food 50 :travel 200 :office 150})
;; => [[:travel 200] [:office 150]]

;; Remove entries where value is nil
(remove (fn [[k v]] (nil? v)) {:a 1 :b nil :c 3})
;; => [[:a 1] [:c 3]]
```

**`keep`** applies a function and returns non-nil results (hybrid of `map` + `filter`). Unlike `filter`, it returns `f`'s results, not the original items. Unlike `map`, it drops nil results. `false` is kept:

```clojure
;; keep only odd numbers, returning them
(keep (fn [x] (when (odd? x) x)) (range 10))
;; => [1 3 5 7 9]

;; identity keeps false but drops nil
(keep identity [false nil 1 2 nil 3])
;; => [false 1 2 3]

;; transform and filter in one step
(keep (fn [x] (when (> x 2) (* x x))) [1 2 3 4 5])
;; => [9 16 25]

;; keep over map entries (sorted - map iteration order varies)
(sort (keep (fn [[k v]] (when (> v 1) k)) {:a 1 :b 2 :c 3}))
;; => [:b :c]
```

#### Transforming

| Function | Signature | Description |
|----------|-----------|-------------|
| `map` | `(map f coll)` | Apply f to each item |
| `map` | `(map f c1 c2)` | Apply f to pairs from c1, c2 |
| `map` | `(map f c1 c2 c3)` | Apply f to triples |
| `mapcat` | `(mapcat f coll)` | Apply f to each item, concatenate results |
| `pmap` | `(pmap f coll)` | Apply f to each item in parallel |
| `pmap` | `(pmap f c1 c2 ...)` | Apply f to zipped items from multiple collections in parallel |
| `pcalls` | `(pcalls f1 f2 ...)` | Execute thunks in parallel |
| `mapv` | `(mapv f coll)` | Like map, returns vector |
| `mapv` | `(mapv f c1 c2)` | Like map with two collections |
| `mapv` | `(mapv f c1 c2 c3)` | Like map with three collections |
| `map-indexed` | `(map-indexed f coll)` | Apply f to index and item |
| `select-keys` | `(select-keys map keys)` | Pick specific keys |

```clojure
(map :name users)                    ; extract :name from each item
(mapcat (fn [x] [x (* x 2)]) [1 2 3])  ; => [1 2 2 4 3 6] (map + flatten)
(pmap :name users)                   ; same, but parallel execution
(pcalls #(tool/get-user) #(tool/get-stats))  ; parallel heterogeneous calls
(mapv :name users)                   ; same, ensures vector
(map-indexed (fn [i x] [i x]) ["a" "b"]) ; => [[0 "a"] [1 "b"]]
(select-keys user [:name :email])    ; pick keys from map

;; Multi-arity map - parallel iteration over collections
(map + [1 2 3] [10 20 30])           ; => [11 22 33]
(map (fn [a b] [a b]) [1 2] [:a :b]) ; => [[1 :a] [2 :b]]
(map + [1 2 3 4] [10 20])            ; => [11 22] (stops at shortest)

;; 3-collection map requires explicit closure for variadic ops
(map (fn [a b c] (+ a b c)) [1 2] [10 20] [100 200])  ; => [111 222]

;; mapcat - apply function and concatenate results (flat_map)
(mapcat (fn [x] (range 0 x)) [2 3 1])   ; => [0 1 0 1 2 0]
(mapcat identity [[1 2] [3 4] [5]])     ; => [1 2 3 4 5] (flatten one level)
(mapcat (fn [x] (if (> x 0) [x] [])) [-1 2 -3 4])  ; => [2 4] (filter + flatten)
```

**Limitation:** Variadic builtins (`+`, `*`, `str`) don't work directly with 3-collection map—use explicit closures. See [#668](https://github.com/andreasronge/ptc_runner/issues/668).

**Note:** Since PTC-Lisp has no lazy sequences (see §13), `map` and `mapv` are functionally identical—both return vectors. `mapv` is provided for Clojure compatibility and to make intent explicit.

**Parallel Map (`pmap`):** Executes the function for each element concurrently using BEAM processes. Useful when the mapping function involves I/O-bound operations (like tool calls) that can benefit from parallelism:

```clojure
;; Process multiple items in parallel - much faster for I/O-bound tasks
(pmap #(tool/fetch-data {:id %}) item-ids)

;; Closures work - captures outer scope at evaluation time
(let [factor 10]
  (pmap #(* % factor) [1 2 3]))    ; => [10 20 30]
```

**pmap semantics:**
- Order is preserved - results match input order
- Shares `map`'s finite seqable contract: `(pmap inc nil)` → `()`, strings map
  over graphemes (`(pmap str "ab")` → `("a" "b")`), and multiple collections
  zip element-wise, truncating to the shortest (`(pmap + [1 2 3] [10 20])` → `(11 22)`)
- Each parallel branch gets a read-only snapshot of the user namespace
- Writes within branches (via `def`) are isolated and discarded
- Errors in any branch propagate to the caller
- Concurrency is bounded to `2 × CPU cores` to prevent resource exhaustion
- Individual tasks timeout after 5 seconds

**Parallel Calls (`pcalls`):** Executes multiple zero-arity functions (thunks) concurrently and returns their results as a vector. Unlike `pmap` which applies one function to many items, `pcalls` runs multiple different functions in parallel:

```clojure
;; Fetch multiple pieces of data in parallel
(let [[user stats config] (pcalls
                            #(tool/get-user {:id data/user-id})
                            #(tool/get-stats {:id data/user-id})
                            #(tool/get-config {}))]
  {:user user :stats stats :config config})

;; Simple parallel computations
(pcalls #(+ 1 1) #(* 2 3) #(- 10 5))    ; => [2 6 5]
```

**pcalls semantics:**
- Order is preserved - results match argument order
- All functions must be zero-arity thunks (use `#()` syntax)
- If any function fails, entire `pcalls` expression fails (atomic)
- Errors include the failed function index and error details
- Each parallel branch gets a read-only snapshot of the user namespace
- Concurrency is bounded to `2 × CPU cores` to prevent resource exhaustion
- Individual tasks timeout after 5 seconds

#### Ordering

| Function | Signature | Description |
|----------|-----------|-------------|
| `sort` | `(sort coll)` | Sort by natural order |
| `sort` | `(sort comparator coll)` | Sort with comparator (`:asc`, `:desc`, a 2-arg fn, or a boolean comparator) |
| `sort-by` | `(sort-by keyfn coll)` | Sort by extracted key |
| `sort-by` | `(sort-by keyfn comp coll)` | Sort with comparator |
| `reverse` | `(reverse coll)` | Reverse order |

**Sortable types:** Numbers, strings, and map entries can be sorted. Numbers use numeric order; strings use lexicographic (alphabetical) order. Sorting mixed types or unsortable types (such as nil) raises a type error.

```clojure
(sort [3 1 2])                ; => [1 2 3]
(sort ["b" "a" "c"])          ; => ["a" "b" "c"]
(sort :desc [1 3 2])          ; => [3 2 1] (Clojure extension)
(sort :asc [3 1 2])           ; => [1 2 3] (Clojure extension)
(sort-by :price products)     ; ascending by price
(sort-by :price > products)   ; descending by price (boolean comparator)
(sort-by :price :desc products) ; descending by price (simplified keyword)
(sort-by :price (fn [a b] (compare b a)) products) ; descending by price (Clojure-style)
(sort-by :name products)      ; alphabetical by name
(sort-by first [["b" 2] ["a" 1] ["c" 3]])  ; => [["a" 1] ["b" 2] ["c" 3]]
(sort-by (fn [x] (nth x 1)) > [["a" 2] ["b" 1] ["c" 3]])  ; descending by second element
(reverse [1 2 3])             ; => [3 2 1]
```

**Note:** `sort`, `sort-by`, and the explicit comparison operators (`>`, `<`, `>=`, `<=`) support strings and mixed scalar values using recoverable runtime term ordering. Prefer numeric-only data when numeric ordering is required.

**Map support:** `sort` and `sort-by` accept maps, treating each entry as a `[key value]` pair. They return a **list** of `[key value]` pairs (not a map) to preserve sort order:

```clojure
;; Sort map entries by key/value pair order
(sort {:b 2 :a 1})
;; => [[:a 1] [:b 2]]

;; Sort map by values (descending)
(sort-by second > {:food 100 :travel 500 :office 200})
;; => [[:travel 500] [:office 200] [:food 100]]

;; Sort map by keys
(sort-by first {:z 1 :a 2 :m 3})
;; => [[:a 2] [:m 3] [:z 1]]
```

#### Subsetting

| Function | Signature | Description |
|----------|-----------|-------------|
| `first` | `(first coll)` | First item or nil |
| `second` | `(second coll)` | Second item or nil |
| `last` | `(last coll)` | Last item or nil |
| `nth` | `(nth coll idx)` | Item at index or nil |
| `rest` | `(rest coll)` | All but first (empty list if none) |
| `butlast` | `(butlast coll)` | All but last (nil if none) |
| `next` | `(next coll)` | All but first (nil if none) |
| `ffirst` | `(ffirst coll)` | First of first |
| `fnext` | `(fnext coll)` | First of next |
| `nfirst` | `(nfirst coll)` | Next of first |
| `nnext` | `(nnext coll)` | Next of next |
| `take` | `(take n coll)` | First n items |
| `drop` | `(drop n coll)` | Skip first n items |
| `nthrest` | `(nthrest coll n)` | Drop first n items (alias for `drop` with swapped args) |
| `nthnext` | `(nthnext coll n)` | Drop first n items, returning a seq or `nil` if empty |
| `take-last` | `(take-last n coll)` | Last n items |
| `drop-last` | `(drop-last coll)` `(drop-last n coll)` | All but last n items (default n=1) |
| `take-while` | `(take-while pred coll)` | Take while pred is true |
| `drop-while` | `(drop-while pred coll)` | Drop while pred is true |
| `distinct` | `(distinct coll)` | Remove duplicates |
| `split-at` | `(split-at n coll)` | Split into `[(take n coll) (drop n coll)]` |
| `split-with` | `(split-with pred coll)` | Split into `[(take-while pred coll) (drop-while pred coll)]` |
| `partition` | `(partition n coll)` | Chunk into groups of n (incomplete groups discarded) |
| `partition` | `(partition n step coll)` | Sliding window chunks (incomplete discarded) |
| `partition` | `(partition n step pad coll)` | Sliding window with pad collection for incomplete groups |
| `partition-all` | `(partition-all n coll)` | Chunk into groups of n (incomplete groups included) |
| `partition-all` | `(partition-all n step coll)` | Sliding window chunks (incomplete included) |
| `partition-by` | `(partition-by f coll)` | Partition when f's return value changes |

```clojure
(first [1 2 3])       ; => 1
(first [])            ; => nil
(second [1 2 3])      ; => 2
(last [1 2 3])        ; => 3
(nth [1 2 3] 1)       ; => 2
(nth [1 2 3] 10)      ; => nil (out of bounds)
(rest [1 2 3])        ; => [2 3]
(rest [])             ; => []
(butlast [1 2 3 4])   ; => [1 2 3]
(butlast [1])         ; => nil
(butlast [])          ; => nil
(next [1 2 3])        ; => [2 3]
(next [])             ; => nil
(next [1])            ; => nil
(ffirst [[1 2] [3]])  ; => 1
(fnext [1 2 3])       ; => 2
(nfirst [[1 2] [3]])  ; => [2]
(nnext [1 2 3 4])     ; => [3 4]
(take 2 [1 2 3 4])    ; => [1 2]
(drop 2 [1 2 3 4])    ; => [3 4]
(distinct [1 2 1 3])  ; => [1 2 3]

;; partition - chunk collection into groups (incomplete discarded)
(partition 2 [1 2 3 4 5 6])          ; => [[1 2] [3 4] [5 6]]
(partition 3 [1 2 3 4 5])            ; => [[1 2 3]] (incomplete discarded)
(partition 2 1 [1 2 3 4])            ; => [[1 2] [2 3] [3 4]] (sliding window)
(partition 3 3 [0] [1 2 3 4 5])      ; => [[1 2 3] [4 5 0]] (pad incomplete with [0])

;; partition-all - like partition but includes incomplete groups
(partition-all 2 [1 2 3 4 5])        ; => [[1 2] [3 4] [5]]
(partition-all 3 [1 2 3 4 5])        ; => [[1 2 3] [4 5]]

;; split-at - split at index
(split-at 2 [1 2 3 4 5])            ; => [[1 2] [3 4 5]]
(split-at 0 [1 2 3])                ; => [[] [1 2 3]]
(split-at -1 [1 2 3])               ; => [[] [1 2 3]] (negative clamps to 0)

;; split-with - split by predicate (takes while true, then rest)
(split-with pos? [1 2 -1 3])        ; => [[1 2] [-1 3]]
(split-with even? [2 4 5 6])        ; => [[2 4] [5 6]]

;; partition-by - partition when function value changes
(partition-by odd? [1 1 2 2 3])     ; => [[1 1] [2 2] [3]]
(partition-by identity [1 1 2 3 3]) ; => [[1 1] [2] [3 3]]

;; dedupe - remove consecutive duplicates
(dedupe [1 1 2 3 3 2])              ; => [1 2 3 2]
(dedupe "aabcc")                    ; => ["a" "b" "c"]

;; keep-indexed - keep non-nil results of (f index item)
(keep-indexed (fn [i v] (if (odd? i) v)) [:a :b :c :d])  ; => [:b :d]
(keep-indexed (fn [i v] (when (even? i) v)) [10 20 30])  ; => [10 30]
```

**take-while and drop-while with keywords:**

```clojure
;; Using keyword directly (checks field truthiness)
(take-while :active [{:active true} {:active true} {:active false}])
;; => [{:active true} {:active true}]

(drop-while :pending [{:pending true} {:pending true} {:pending false}])
;; => [{:pending false}]
```

#### Combining

| Function | Signature | Description |
|----------|-----------|-------------|
| `cons` | `(cons x seq)` | Prepend item to sequence |
| `conj` | `(conj coll x ...)` | Add elements to collection |
| `concat` | `(concat coll1 coll2 ...)` | Join collections |
| `into` | `(into to from)` | Pour from into to |
| `flatten` | `(flatten coll)` | Flatten nested collections |
| `interleave` | `(interleave c1 c2 ...)` | Interleave collections |
| `interpose` | `(interpose sep coll)` | Insert separator between elements |
| `zip` | `(zip c1 c2)` | Combine into pairs |
| `zipmap` | `(zipmap keys vals)` | Create map from keys and values seqs |
| `hash-set` | `(hash-set & items)` | Create a set from the given items |
| `empty` | `(empty coll)` | Return empty collection of same type |
| `peek` | `(peek coll)` | Return last element without removing |
| `pop` | `(pop coll)` | Return collection without last element |
| `subvec` | `(subvec v start)` `(subvec v start end)` | Return subvector (clamps indices) |
| `disj` | `(disj set x ...)` | Remove elements from set |

```clojure
(cons 0 [1 2 3])           ; => [0 1 2 3]
(cons 0 nil)               ; => [0]
(conj [1 2] 3)             ; => [1 2 3]
(conj #{1 2} 3)            ; => #{1 2 3}
(conj {:a 1} [:b 2])       ; => {:a 1 :b 2}
(concat [1 2] [3 4])       ; => [1 2 3 4]
(into [] [1 2 3])          ; => [1 2 3]
(into [] {:a 1 :b 2})       ; => [[:a 1] [:b 2]]
(into #{} [1 2 2 3])       ; => #{1 2 3}
(into {} [[:a 1] [:b 2]])  ; => {:a 1 :b 2}
(into #{} {:a 1})          ; => #{[:a 1]}
(into {} #{[:a 1]})        ; => {:a 1}
(flatten [[1 2] [3 [4]]])  ; => [1 2 3 4]
(interpose ", " ["a" "b" "c"]) ; => ["a" ", " "b" ", " "c"]
(zip [1 2] [:a :b])        ; => [[1 :a] [2 :b]]
(zipmap [:a :b :c] [1 2 3]) ; => {:a 1 :b 2 :c 3}
(zipmap [:a :b] [1 2 3])   ; => {:a 1 :b 2} (truncates to shorter)
(empty [1 2 3])             ; => []
(empty {:a 1})              ; => {}
(empty nil)                 ; => nil
(peek [1 2 3])              ; => 3
(peek [])                   ; => nil
(pop [1 2 3])               ; => [1 2]
(pop [])                    ; => nil
(subvec [0 1 2 3 4] 1 3)   ; => [1 2]
(subvec [0 1 2 3 4] 2)     ; => [2 3 4]
(disj #{1 2 3} 2)           ; => #{1 3}
(disj #{1 2 3} 2 3)         ; => #{1}
```

#### Combinatorial

| Function | Signature | Description |
|----------|-----------|-------------|
| `combinations` | `(combinations coll n)` | Generate all n-combinations |

```clojure
(combinations [1 2 3] 2)   ; => [[1 2] [1 3] [2 3]]
(combinations [1 2 3 4] 3) ; => [[1 2 3] [1 2 4] [1 3 4] [2 3 4]]
(combinations [1 2 3] 0)   ; => [[]] (empty subset)
(combinations [1 2] 3)     ; => [] (n > length)

;; Practical: find pairs that sum to target
(->> (combinations [1 2 3 4 5] 2)
     (filter (fn [[a b]] (= (+ a b) 6))))
;; => [[1 5] [2 4]]
```

#### Tree Traversal

| Function | Signature | Description |
|----------|-----------|-------------|
| `walk` | `(walk inner outer form)` | Generic tree walker - applies inner to children, outer to result |
| `prewalk` | `(prewalk f form)` | Transform tree top-down (pre-order traversal) |
| `postwalk` | `(postwalk f form)` | Transform tree bottom-up (post-order traversal) |
| `tree-seq` | `(tree-seq branch? children root)` | Flatten tree to depth-first sequence |

These functions work on all collection types: vectors, maps, and sets.

```clojure
;; walk: apply inner to each element, then outer to the result
(walk inc #(apply + %) [1 2 3])  ; => 9 (inc each, then sum)
(walk identity identity [1 2 3]) ; => [1 2 3]
(walk inc identity #{1 2 3})     ; => #{2 3 4} (sets are preserved)

;; prewalk: transform nodes top-down
(prewalk #(if (number? %) (inc %) %) [1 [2 3]])
; => [2 [3 4]]

;; postwalk: transform nodes bottom-up
(postwalk #(if (number? %) (inc %) %) [1 [2 3]])
; => [2 [3 4]]

;; postwalk can aggregate after children are processed
(postwalk #(if (vector? %) (apply + %) %) [[1 2] [3 4]])
; => 10 (inner lists sum first: [3 7], then outer sums: 10)

;; tree-seq: flatten hierarchical data
(let [tree {:id 1 :children [{:id 2 :children []} {:id 3 :children []}]}]
  (map :id (tree-seq :children :children tree)))
; => [1 2 3]

;; Practical: find all nodes matching criteria in a tree
(let [tree {:name "root" :value 10 :children [
             {:name "a" :value 5 :children []}
             {:name "b" :value 15 :children []}]}]
  (->> (tree-seq :children :children tree)
       (filter #(> (:value %) 10))
       (map :name)))
; => ["b"]

;; Transform all values in a nested structure
(prewalk #(if (and (map? %) (:value %))
             (update % :value inc)
             %)
         {:value 1 :nested {:value 2}})
; => {:value 2 :nested {:value 3}}
```

#### Conversion

| Function | Signature | Description |
|----------|-----------|-------------|
| `seq` | `(seq coll)` | Convert to sequence (nil if empty) |

The `seq` function converts a collection to a sequence:
- **Lists**: Returns the list unchanged, or nil if empty
- **Strings**: Returns a list of characters (graphemes), or nil if empty
- **Sets**: Returns a list of elements, or nil if empty
- **Maps**: Returns a list of `[key value]` pairs, or nil if empty
- **nil**: Returns nil

```clojure
(seq [1 2 3])              ; => [1 2 3]
(seq [])                   ; => nil
(seq "hello")              ; => ["h" "e" "l" "l" "o"]
(seq "")                   ; => nil
(seq #{1 2 3})             ; => [1 2 3] or another order (sets are unordered)
(seq {})                   ; => nil
(seq {:a 1 :b 2})          ; => [[:a 1] [:b 2]]
(count (seq "abc"))        ; => 3 (iterate over characters)
```

#### Aggregation

| Function | Signature | Description |
|----------|-----------|-------------|
| `count` | `(count coll)` | Number of items |
| `reduce` | `(reduce f coll)` | Fold using the first element as the initial value |
| `reduce` | `(reduce f init coll)` | Fold collection with an explicit initial value |
| `sum` | `(sum coll)` | Sum of numbers |
| `avg` | `(avg coll)` | Average of numbers |
| `sum-by` | `(sum-by key coll)` | Sum field values |
| `avg-by` | `(avg-by key coll)` | Average field values |
| `min-by` | `(min-by f coll)` | Item with minimum f-value in coll |
| `min-by` | `(min-by f x y & more)` | Minimum item among individual args |
| `max-by` | `(max-by f coll)` | Item with maximum f-value in coll |
| `max-by` | `(max-by f x y & more)` | Maximum item among individual args |
| `distinct-by` | `(distinct-by key coll)` | Items with unique field values |
| `min-key` | `(min-key f x y & more)` | Return x for which (f x) is least |
| `max-key` | `(max-key f x y & more)` | Return x for which (f x) is greatest |
| `group-by` | `(group-by keyfn coll)` | Group items by key |
| `frequencies` | `(frequencies coll)` | Count occurrences of each item |

```clojure
(count [1 2 3])                   ; => 3
(reduce + 0 [1 2 3])              ; => 6
(reduce - 10 [1 2 3])             ; => 4 (10 - 1 - 2 - 3, Clojure style: f receives (acc, elem))

;; reduce on maps (v is [key value] pair)
;; NOTE: 3-arg form is preferred for maps as the 2-arg form uses the first [k v] pair as init.
(reduce (fn [acc [k v]] (+ acc v)) 0 {:a 1 :b 2}) ; => 3

;; reduce on strings (iterates over graphemes)
(reduce (fn [acc x] (str acc "-" x)) "a" "bc") ; => "a-b-c"

;; reduce on sets
(reduce + 0 #{1 2 3})              ; => 6

;; sum and avg for simple number collections
(sum [1 2 3 4 5])                 ; => 15
(avg [1 2 3 4 5])                 ; => 3.0
(avg [10 20])                     ; => 15.0
(avg [])                          ; => nil (empty collection)
(sum [])                          ; => 0

(sum-by :amount expenses)         ; sum of :amount fields
(avg-by :price products)          ; average of :price fields
(min-by :price products)          ; item with lowest price
(max-by :years employees)         ; item with highest years
(group-by :category products)     ; map of category -> items
(distinct-by :category products)  ; one item per category (first occurrence)
(frequencies [:a :b :a :c :b :a]) ; => {:a 3, :b 2, :c 1}
(frequencies "hello")                 ; => {"h" 1, "e" 1, "l" 2, "o" 1}
(frequencies (map :status orders))    ; count orders by status
(min-by first [["b" 2] ["a" 1]])  ; => ["a" 1] (item with minimum first element)
(max-by (fn [x] (nth x 1)) [["a" 2] ["b" 3]])  ; item with maximum second element
(sum-by (fn [x] (nth x 1)) [["a" 2] ["b" 3]])  ; => 5 (sum second elements)
(group-by first [["a" 1] ["a" 2] ["b" 3]])  ; {"a" [["a" 1] ["a" 2]], "b" [["b" 3]]}
(distinct-by first [["a" 1] ["a" 2] ["b" 3]])  ; [["a" 1] ["b" 3]] (first of each key)

;; max-key / min-key - compare variadic args using function
(max-key count "a" "abc" "ab")              ; => "abc" (longest string)
(min-key count "abc" "a" "ab")              ; => "a" (shortest string)
(max-key #(nth % 1) ["a" 1] ["b" 5] ["c" 3]) ; => ["b" 5]

;; Common pattern: find map entry with max/min value using apply
(apply max-key second (seq {:a 3 :b 7 :c 2}))  ; => [:b 7]

;; Note: max-key/min-key are variadic (take individual items) - use apply to spread a collection
;; max-by/min-by take a collection directly - no apply needed: (max-by :key coll)
```

#### Predicates on Collections

| Function | Signature | Description |
|----------|-----------|-------------|
| `empty?` | `(empty? coll)` | True if empty or nil |
| `not-empty` | `(not-empty coll)` | `coll` if not empty, else `nil` |
| `some` | `(some pred coll)` | First truthy result of pred, or nil |
| `some` | `(some :key coll)` | First truthy `:key` value, or nil |
| `every?` | `(every? pred coll)` | True if all match |
| `every?` | `(every? :key coll)` | True if all have truthy `:key` |
| `not-any?` | `(not-any? pred coll)` | True if none match |
| `not-any?` | `(not-any? :key coll)` | True if none have truthy `:key` |
| `not-every?` | `(not-every? pred coll)` | True if not all match (complement of `every?`) |
| `not-every?` | `(not-every? :key coll)` | True if not all have truthy `:key` |
| `distinct?` | `(distinct? x y ...)` | True if all arguments are distinct |
| `contains?` | `(contains? coll key)` | True if key/element exists (maps, sets, lists) |

```clojure
(empty? [])                        ; => true
(empty? nil)                       ; => true
(not-empty [1 2])                  ; => [1 2]
(not-empty [])                     ; => nil
(not-empty nil)                    ; => nil
(some :admin users)                ; any admins? (keyword shorthand)
(every? :active users)             ; all active? (keyword shorthand)
(not-any? :error items)            ; no errors?
(contains? {:a 1} :a)              ; => true
(contains? {:a 1} :b)              ; => false
(contains? ["a" "b" "c"] "b")      ; => true (works on lists too)
(contains? ["a" "b" "c"] "x")      ; => false
```

#### Sequence Generation

| Function | Signature | Description |
|----------|-----------|-------------|
| `range` | `(range end)` | Returns sequence from 0 to end (exclusive) |
| `range` | `(range start end)` | Returns sequence from start to end (exclusive) |
| `range` | `(range start end step)` | Returns sequence with specific step |

```clojure
(range 5)                          ; => [0 1 2 3 4]
(range 5 10)                       ; => [5 6 7 8 9]
(range 0 10 2)                     ; => [0 2 4 6 8]
(range 10 0 -2)                    ; => [10 8 6 4 2]
(range 5 5)                        ; => []
(take 3 (range 1 5 0))             ; => [1 1 1]
```

**Note:** Unlike Clojure, `range` in PTC-Lisp is always finite and **requires at least one argument**. The zero-arity `(range)` which produces an infinite sequence is not supported because PTC-Lisp does not support lazy sequences. A direct zero-step `(range start end 0)` also raises unless it is consumed by bounded `take`.

### 8.2 Map Operations

| Function | Signature | Description |
|----------|-----------|-------------|
| `get` | `(get m key)` | Get value by key |
| `get` | `(get m key default)` | Get with default |
| `get-in` | `(get-in m path)` | Get nested value |
| `get-in` | `(get-in m path default)` | Get nested with default |
| `assoc` | `(assoc m key val)` | Add/update key |
| `assoc-in` | `(assoc-in m path val)` | Add/update nested |
| `update` | `(update m key f)` | Update value with function |
| `update` | `(update m key f & args)` | Update with extra args passed to f |
| `update-in` | `(update-in m path f)` | Update nested with function |
| `update-in` | `(update-in m path f & args)` | Update nested with extra args |
| `dissoc` | `(dissoc m key ...)` | Remove one or more keys |
| `merge` | `(merge m1 m2 ...)` | Merge maps (later wins) |
| `hash-map` | `(hash-map & kvs)` | Build a map from alternating key/value args (equivalent to a map literal) |
| `array-map` | `(array-map & kvs)` | Alias for `hash-map` |
| `select-keys` | `(select-keys m keys)` | Pick specific keys |
| `keys` | `(keys m)` | Get all keys |
| `vals` | `(vals m)` | Get all values |
| `entries` | `(entries m)` | Get all `[key value]` pairs as a list |
| `update-vals` | `(update-vals m f)` | Apply f to each value (matches Clojure 1.11) |
| `update-keys` | `(update-keys m f)` | Apply f to each key (collision: retained value unspecified) |
| `merge-with` | `(merge-with f m1 m2 ...)` | Merge maps with combining function for duplicates |
| `reduce-kv` | `(reduce-kv f init m)` | Reduce map with f receiving (acc, key, val) |

```clojure
(get {:a 1} :a)                    ; => 1
(get {:a 1} :b "default")          ; => "default"
(get-in {:user {:name "A"}} [:user :name])  ; => "A"
(assoc {:a 1} :b 2)                ; => {:a 1 :b 2}
(assoc-in {} [:user :name] "Bob")  ; => {:user {:name "Bob"}}
(update {:n 1} :n inc)             ; => {:n 2}
(update {:n 1} :n + 5)             ; => {:n 6} - extra args passed to f
(update {:n nil} :n (fnil inc 0))  ; => {:n 1} - fnil with 1-arity fn
(update {:n nil} :n (fnil + 0) 5)  ; => {:n 5} - fnil with 2-arity fn + extra arg
(update-in {:a {:b 1}} [:a :b] + 10) ; => {:a {:b 11}}
(dissoc {:a 1 :b 2} :b)            ; => {:a 1}
(dissoc {:a 1 :b 2 :c 3} :a :b)    ; => {:c 3}
(hash-map :a 1 :b 2)               ; => {:a 1 :b 2}
(merge {:a 1} {:b 2} {:a 3})       ; => {:a 3 :b 2}
(select-keys {:a 1 :b 2 :c 3} [:a :c])  ; => {:a 1 :c 3}
(keys {:a 1 :b 2})                 ; => [:a :b]
(vals {:a 1 :b 2})                 ; => [1 2]
(entries {:a 1 :b 2})              ; => [[:a 1] [:b 2]]

;; update-vals: apply function to each value (matches Clojure 1.11)
(update-vals {:a 1 :b 2} inc)      ; => {:a 2 :b 3}

;; Common pattern: count items per group after group-by
;; Note: Use -> (not ->>) since map is first argument
(-> orders
    (group-by :status)
    (update-vals count))           ; => ...

;; update-keys: apply function to each key
(update-keys {:a 1 :b 2} str)     ; => {":a" 1 ":b" 2}

;; merge-with: merge maps with combining function for duplicate keys
(merge-with + {:a 1 :b 2} {:a 3 :c 4})  ; => {:a 4 :b 2 :c 4}
(merge-with + {:a 1} {:a 2} {:a 3})     ; => {:a 6}

;; reduce-kv: reduce over map key-value pairs
(reduce-kv (fn [acc k v] (+ acc v)) 0 {:a 1 :b 2 :c 3})  ; => 6
(reduce-kv (fn [acc k v] (assoc acc k (* v 2))) {} {:a 1 :b 2}) ; => {:a 2 :b 4}
```

**List Index Support:**

`get-in`, `assoc`, `assoc-in`, and `update-in` support numeric indices for list/vector access:

```clojure
(get-in {:results [{:title "A"}]} [:results 0 :title])  ; => "A"
(get-in [1 2 3] [0])                                     ; => 1
(get-in [[1 2] [3 4]] [1 0])                             ; => 3
(get-in [1 2 3] [10])                                    ; => nil (out of bounds)
(get-in [1 2 3] [-1])                                    ; => nil (negative not supported)

(assoc [1 2 3] 1 5)                                      ; => [1 5 3]
(assoc-in [1 2 3] [1] 99)                                ; => [1 99 3]
(assoc-in [[1 2] [3 4]] [0 1] 99)                        ; => [[1 99] [3 4]]
(update-in [1 2 3] [1] inc)                              ; => [1 3 3]
```

**Note:** `assoc`, `assoc-in`, and `update-in` raise `ArgumentError` for out-of-bounds indices.

### 8.3 String Functions

| Function | Signature | Description |
|----------|-----------|-------------|
| `str` | `(str ...)` | Convert and concatenate to string |
| `pr-str` | `(pr-str ...)` | Readable string representation (strings quoted, nil as "nil", space-separated) |
| `subs` | `(subs s start)` | Substring from index to end |
| `subs` | `(subs s start end)` | Substring from start to end |
| `split` | `(split s re-or-char)` | Split string on a regex; single-char string ok, multi-char string signals `:type_error` |
| `split-lines` | `(split-lines s)` | Split string into lines (\n or \r\n) |
| `join` | `(join separator coll)` | Join collection elements with separator |
| `join` | `(join coll)` | Join collection elements (no separator) |
| `trim` | `(trim s)` | Remove leading/trailing whitespace |
| `triml` | `(triml s)` | Remove leading whitespace |
| `trimr` | `(trimr s)` | Remove trailing whitespace |
| `trim-newline` | `(trim-newline s)` | Remove trailing newline/carriage-return characters |
| `blank?` | `(blank? s)` | True if `s` is nil, empty, or only whitespace |
| `replace` | `(replace s pattern replacement)` | Replace all occurrences |
| `upcase` / `upper-case` | `(upcase s)` | Convert to uppercase |
| `downcase` / `lower-case` | `(downcase s)` | Convert to lowercase |
| `starts-with?` | `(starts-with? s prefix)` | Check if string starts with prefix |
| `ends-with?` | `(ends-with? s suffix)` | Check if string ends with suffix |
| `includes?` | `(includes? s substring)` | Check if string contains substring |
| `index-of` | `(index-of s value)` | Index of first occurrence, or `nil` if not found |
| `index-of` | `(index-of s value from-index)` | Index of first occurrence from position |
| `last-index-of` | `(last-index-of s value)` | Index of last occurrence, or `nil` if not found |
| `last-index-of` | `(last-index-of s value from-index)` | Index of last occurrence up to position |
| `format` | `(format fmt-string & args)` | Java-style format string |
| `name` | `(name x)` | Returns name string of keyword or string |

**Type coercion:** `str` converts values to strings using these rules:
- `nil` → `""`
- `true` / `false` → `"true"` / `"false"`
- Numbers → decimal representation (e.g., `42` → `"42"`, `3.14` → `"3.14"`)
- Strings → unchanged
- Keywords → `:keyword` (with leading colon)
- Collections → string representation

```clojure
(str "hello")                  ; => "hello"
(str "Hello" " " "World")      ; => "Hello World"

(subs "hello" 1)               ; => "ello"
(subs "hello" 1 4)             ; => "ell"
```

**PTC-Lisp specific string examples:**
- `(str)` → `""` (empty call)
- `(str 42)` → `"42"` (number conversion)
- `(str true)` → `"true"` (boolean conversion)
- `(str :user)` → `":user"` (keyword with colon)
- `(str nil "x")` → `"x"` (nil coerced to empty string)
- `(str {:a 1})` → `"{:a 1}"` (collections use Clojure syntax)
- `(pr-str "hello")` → `"\"hello\""` (string gets quoted)
- `(pr-str nil)` → `"nil"` (nil as readable literal)
- `(pr-str 1 "a")` → `"1 \"a\""` (space-separated, strings quoted)
- `(split "a,b,c" ",")` → `["a" "b" "c"]` (single-char string delimiter)
- `(split "hello" "")` → `["h" "e" "l" "l" "o"]` (split into characters)
- `(split "a,,b" ",")` → `["a" "" "b"]` (preserves empty elements)
- `(split "a--b--c" #"--")` → `["a" "b" "c"]` (multi-char delimiters need a regex)
- `(split-lines "a\nb\r\nc")` → `["a" "b" "c"]` (split by line endings)
- `(split-lines "a\n\n\n")` → `["a"]` (discards trailing empty lines)
- `(join ", " ["a" "b" "c"])` → `"a, b, c"` (join with separator)
- `(join "-" [1 2 3])` → `"1-2-3"` (numeric types converted)
- `(trim "\n\tworld\r\n")` → `"world"` (remove all whitespace)
- `(replace "hello" "l" "L")` → `"heLLo"` (replace all occurrences)
- `(replace "aaa" "a" "b")` → `"bbb"` (replace pattern)
- `(upcase "hello")` → `"HELLO"` (uppercase conversion)
- `(upper-case "world")` → `"WORLD"` (alias for upcase)
- `(downcase "HELLO")` → `"hello"` (lowercase conversion)
- `(lower-case "WORLD")` → `"world"` (alias for downcase)
- `(starts-with? "hello" "he")` → `true` (prefix check)
- `(starts-with? "hello" "lo")` → `false` (does not start with)
- `(starts-with? "hello" "")` → `true` (empty prefix always matches)
- `(ends-with? "hello" "lo")` → `true` (suffix check)
- `(ends-with? "hello" "he")` → `false` (does not end with)
- `(ends-with? "hello" "")` → `true` (empty suffix always matches)
- `(includes? "hello" "ll")` → `true` (substring check)
- `(includes? "hello" "x")` → `false` (does not contain)
- `(includes? "hello" "")` → `true` (empty substring always matches)
- `(index-of "hello" "l")` → `2` (first occurrence)
- `(index-of "hello" "x")` → `nil` (not found returns nil)
- `(index-of "hello" "l" 3)` → `3` (search from position)
- `(index-of "hello" "")` → `0` (empty value returns 0)
- `(last-index-of "hello" "l")` → `3` (last occurrence)
- `(last-index-of "hello" "x")` → `nil` (not found returns nil)
- `(last-index-of "hello" "l" 2)` → `2` (search up to position)
- `(last-index-of "hello" "")` → `5` (empty value returns string length)
- `(last-index-of "aaa" "aa")` → `1` (overlapping matches handled correctly)
- `(format "%s has %d items" "cart" 3)` → `"cart has 3 items"` (string and integer formatting)
- `(format "%.2f" 3.14159)` → `"3.14"` (float with precision)
- `(format "%d in hex is %x" 255 255)` → `"255 in hex is ff"` (decimal and hex)
- `(format "%e" 1234.5)` → `"1.234500e+03"` (scientific notation)
- `(format "%o" 8)` → `"10"` (octal)
- `(format "100%%")` → `"100%"` (literal percent)
- `(format "%s" nil)` → `""` (nil coerced via str — Clojure returns `"null"`, see divergence)
- `(format "%s and %s" "a" "b" "c")` → `"a and b"` (extra args ignored)
- `(format "%f" ##Inf)` → `"Infinity"` (special values work with %f)
- `(format "%d" ##Inf)` → ERROR (special values error with %d)
- `(name :foo)` → `"foo"` (keyword name without colon)
- `(name "bar")` → `"bar"` (string passes through)
- `(name nil)` → ERROR (nil not supported)
- `(name 42)` → ERROR (numbers not supported)

**`format`** supports these format specifiers: `%s` (string via `str`), `%d` (integer), `%f`/`%.Nf` (float with optional precision, default 6 decimal places), `%e` (scientific notation), `%x` (hex), `%o` (octal), `%%` (literal `%`). Extra arguments are ignored. Special values (`##Inf`, `##-Inf`, `##NaN`) work with `%s` and `%f`/`%e` but raise an error with `%d`. **Divergence:** `(format "%s" nil)` returns `""` (Clojure returns `"null"`).

**`name`** returns the name string of a keyword (without the leading colon) or passes strings through unchanged. Raises an error on nil, numbers, booleans, and special values.

**Note:** All string indices are grapheme-based (not byte offsets or UTF-16 code units), consistent with `subs`, `count`, and other PTC-Lisp string functions.

### 8.4 Arithmetic

| Function | Signature | Description |
|----------|-----------|-------------|
| `+` | `(+ x y ...)` | Addition |
| `-` | `(- x y ...)` | Subtraction |
| `*` | `(* x y ...)` | Multiplication |
| `/` | `(/ x y)` | Division (always returns float) |
| `quot` | `(quot x y)` | Integer division (truncated toward zero) |
| `mod` | `(mod x y)` | Modulo (floored division, result sign matches divisor) |
| `rem` | `(rem x y)` | Remainder (truncated division, result sign matches dividend) |
| `inc` | `(inc x)` | Add 1 |
| `dec` | `(dec x)` | Subtract 1 |
| `abs` | `(abs x)` | Absolute value |
| `sqrt` | `(sqrt x)` | Square root (returns float; `##NaN` for negatives) |
| `pow` | `(pow x y)` | Exponentiation (returns float) |
| `trunc` | `(trunc x)` | Truncate toward zero |
| `compare` | `(compare x y)` | Numeric comparison: `-1` if `x < y`, `0` if `x == y`, `1` if `x > y`. Only supports numbers in PTC-Lisp. |
| `max` | `(max x y ...)` | Maximum value |
| `min` | `(min x y ...)` | Minimum value |
| `floor` | `(floor x)` | Round toward -∞ |
| `ceil` | `(ceil x)` | Round toward +∞ |
| `round` | `(round x)` | Round to nearest integer |
| `float` | `(float x)` | Alias for double (Clojure compat) |
| `double` | `(double x)` | Type coercion (to float) |
| `int` | `(int x)` | Type coercion (to integer) |
| `keyword` | `(keyword x)` | Type coercion (string to keyword) |

**Special Value Behavior:**
- **NaN Propagation**: Any arithmetic operation involving `Double/NaN` returns `Double/NaN`.
- **Division by Zero**: An **integer** zero divisor — `(/ n 0)` — raises an `arithmetic-error` (Clojure conformance). A **float** zero divisor follows IEEE 754: `(/ n 0.0)` returns `Double/POSITIVE_INFINITY` (if `n > 0`), `Double/NEGATIVE_INFINITY` (if `n < 0`), or `Double/NaN` (if `n = 0`).
- **Indeterminate Forms**: Operations like `(- Double/POSITIVE_INFINITY Double/POSITIVE_INFINITY)` or `(* Double/POSITIVE_INFINITY 0)` return `Double/NaN`.
- **Coercion**: Converting `Infinity` to `int` raises an `arithmetic-error`; `(int ##NaN)` returns `0`, matching JVM int coercion.

```clojure
(+ 1 2 3)       ; => 6
(- 10 3)        ; => 7
(* 2 3 4)       ; => 24
(/ 10 2)        ; => 5.0
(/ 10 3)        ; => 3.333...
(quot 10 3)     ; => 3
(quot -7 2)     ; => -3   (truncates toward zero)
(mod 10 3)      ; => 1
(mod -10 3)     ; => 2   (sign matches divisor)
(rem 10 3)      ; => 1
(rem -10 3)     ; => -1  (sign matches dividend)
(inc 5)         ; => 6
(dec 5)         ; => 4
(abs -5)        ; => 5
(max 1 5 3)     ; => 5
(min 1 5 3)     ; => 1
(floor 3.7)     ; => 3
(ceil 3.2)      ; => 4
(round 3.5)     ; => 4
(double 5)      ; => 5.0
(int 3.7)       ; => 3
(int ##NaN)     ; => 0
(keyword "foo")  ; => :foo
(keyword :bar)   ; => :bar
(keyword nil)    ; => nil
(keyword "a/b")  ; => ERROR (no `/` per DIV-13)
(keyword "")     ; => ERROR (empty string invalid)
(keyword "+")    ; => ERROR (operator chars not allowed)
(keyword ##Inf)  ; => ERROR (special values rejected)

(/ 1.0 0.0)                         ; => ##Inf
(/ 0.0 0.0)                         ; => ##NaN
(sqrt -1)                           ; => ##NaN
(+ Double/POSITIVE_INFINITY 1)      ; => ##Inf
(* Double/NaN 10)                   ; => ##NaN
(int Double/POSITIVE_INFINITY)      ; => ARITHMETIC ERROR
```

**Division behavior:** The `/` operator always returns a float, even for exact divisions. For integer division, use `quot` which truncates toward zero—useful for index calculations like `(take (quot n 2) coll)`. Division by zero returns `Infinity`, `-Infinity`, or `NaN` as per IEEE 754 standard for floats. Converting `Infinity` to `int` raises an `arithmetic-error`; `(int ##NaN)` returns `0`.

**`keyword` coercion:** Coerces a string to a keyword, passes keywords through unchanged, and returns `nil` for `nil`. Validates that the name starts with a letter and contains only letters, digits, `-`, `_`, `?`, `!`—no `/` (per DIV-13), no spaces, no empty strings, and no operator characters (`+`, `*`, `<`, `>`, `=`). Special numeric values (`##Inf`, `##-Inf`, `##NaN`) are rejected. Coercion never grows the BEAM atom table: names in the bounded vocabulary become atoms, every other name becomes a runtime keyword struct.

### 8.4a Bitwise Operations

Integer-only bit manipulation, mirroring `clojure.core`. All arguments must be integers; non-integer arguments raise a `type-error`. For the single-bit ops (`bit-set`, `bit-clear`, `bit-flip`, `bit-test`), `n` is the zero-based bit index and must be a non-negative integer.

| Function | Signature | Description |
|----------|-----------|-------------|
| `bit-and` | `(bit-and x & more)` | Bitwise AND of integers |
| `bit-or` | `(bit-or x & more)` | Bitwise OR of integers |
| `bit-xor` | `(bit-xor x & more)` | Bitwise exclusive OR of integers |
| `bit-and-not` | `(bit-and-not x & more)` | Bitwise AND of `x` with the complement of each subsequent argument |
| `bit-not` | `(bit-not x)` | Bitwise complement (two's complement) of an integer |
| `bit-shift-left` | `(bit-shift-left x n)` | Shift `x` left by `n` bits |
| `bit-shift-right` | `(bit-shift-right x n)` | Arithmetic (sign-extending) shift `x` right by `n` bits |
| `bit-set` | `(bit-set x n)` | Set bit `n` of `x` to 1 |
| `bit-clear` | `(bit-clear x n)` | Clear bit `n` of `x` (set it to 0) |
| `bit-flip` | `(bit-flip x n)` | Flip bit `n` of `x` |
| `bit-test` | `(bit-test x n)` | Return `true` if bit `n` of `x` is set, else `false` |

```clojure
(bit-and 12 10)         ; => 8
(bit-or 12 10)          ; => 14
(bit-xor 12 10)         ; => 6
(bit-and-not 15 9)      ; => 6
(bit-not 0)             ; => -1
(bit-shift-left 1 4)    ; => 16
(bit-shift-right 256 4) ; => 16
(bit-set 0 3)           ; => 8
(bit-clear 15 1)        ; => 13
(bit-flip 0 2)          ; => 4
(bit-test 5 0)          ; => true
(bit-test 5 1)          ; => false
```

> **BEAM divergence:** Erlang/BEAM integers are arbitrary-precision, so the shift amount is *not* taken modulo 64 and `bit-shift-left` results can grow without bound (unlike the JVM's fixed 64-bit longs). `unsigned-bit-shift-right` is intentionally not provided because it has no defined meaning without a fixed integer width.

### 8.5 Comparison

| Function | Signature | Description |
|----------|-----------|-------------|
| `=` | `(= x)`, `(= x y & more)` | Equality |
| `==` | `(== x)`, `(== x y & more)` | Numeric equality (alias for `=`; unlike Clojure, accepts any type rather than throwing on non-numbers — DIV-10) |
| `not=` | `(not= x)`, `(not= x y & more)` | Inequality |
| `<` | `(< x)`, `(< x y & more)` | Less than |
| `>` | `(> x)`, `(> x y & more)` | Greater than |
| `<=` | `(<= x)`, `(<= x y & more)` | Less or equal |
| `>=` | `(>= x)`, `(>= x y & more)` | Greater or equal |

**Note:** Comparison and equality operators are variadic but require at least one argument. Ordered comparisons compare adjacent pairs, so `(< 1 2 3)` is equivalent to `(and (< 1 2) (< 2 3))`.

```clojure
(= 1 1)         ; => true
(= 1 1 1)       ; => true
(= 1 2)         ; => false
(not= 1 2)      ; => true
(< 1 2)         ; => true
(< 1 2 3)       ; => true
(> 3 2)         ; => true
(<= 1 1)        ; => true
(>= 3 2)        ; => true

;; Special Value Comparisons (IEEE 754)
(< 1.0 Double/POSITIVE_INFINITY)     ; => true
(> -1.0 Double/NEGATIVE_INFINITY)    ; => true
(= Double/NaN Double/NaN)            ; => false
(< Double/NaN 0.0)                   ; => false
(>= Double/NaN 0.0)                  ; => false
```

### 8.6 Logic

| Function | Signature | Description |
|----------|-----------|-------------|
| `and` | `(and x y ...)` | Logical AND (short-circuits) |
| `or` | `(or x y ...)` | Logical OR (short-circuits) |
| `not` | `(not x)` | Logical NOT |
| `identity` | `(identity x)` | Returns argument unchanged |

```clojure
(and true true)     ; => true
(and true false)    ; => false
(and nil "x")       ; => nil (short-circuits)
(or false true)     ; => true
(or nil false "x")  ; => "x" (returns first truthy)
(not true)          ; => false
(not nil)           ; => true
(identity 42)       ; => 42
```

**`identity` function:** Returns its argument unchanged. Useful as a default function argument, for passing to higher-order functions, or in pipelines where no transformation is needed.

### 8.7 Type Predicates

| Function | Description |
|----------|-------------|
| `nil?` | Is nil? |
| `some?` | Is not nil? |
| `boolean?` | Is boolean? |
| `number?` | Is number? |
| `int?` | Is integer? |
| `integer?` | Is integer? (alias for `int?`) |
| `float?` | Is float? |
| `double?` | Is float? (alias for `float?`) |
| `string?` | Is string? |
| `char?` | Is single-character string? (See §3.5) |
| `keyword?` | Is keyword? |
| `vector?` | Is vector? |
| `map?` | Is map? |
| `set?` | Is set? |
| `fn?` | Is function? |
| `false?` | Is exactly `false`? |
| `true?` | Is exactly `true`? |
| `symbol?` | Always false — PTC-Lisp uses keywords, not symbols (see [DIV-19](clojure-conformance-gaps.md#div-19-symbol-always-returns-false)) |
| `decimal?` | Always false — BEAM has no BigDecimal (see [DIV-20](clojure-conformance-gaps.md#div-20-decimal-and-ratio-always-return-false)) |
| `ratio?` | Always false — BEAM has no ratio type (see [DIV-20](clojure-conformance-gaps.md#div-20-decimal-and-ratio-always-return-false)) |
| `rational?` | Is integer? — integers are the only BEAM rationals (see [DIV-20](clojure-conformance-gaps.md#div-20-decimal-and-ratio-always-return-false)) |
| `nat-int?` | Is non-negative integer? (>= 0) |
| `neg-int?` | Is negative integer? |
| `pos-int?` | Is positive integer? (> 0) |
| `infinite?` | Is positive or negative infinity? |
| `NaN?` | Is NaN? |
| `coll?` | Is collection? (vectors, maps, or sets) |
| `sequential?` | Is ordered collection? (vectors only) |
| `seq?` | Is sequence? (vectors only; same as `sequential?` in PTC-Lisp) |
| `associative?` | Supports `assoc`? (vectors and maps) |
| `counted?` | Has O(1) count? (vectors, maps, sets, strings) |
| `indexed?` | Supports `nth`? (vectors and strings) |
| `reversible?` | Supports `reverse`? (vectors and strings) |
| `sorted?` | Always false — no sorted collections in PTC-Lisp |
| `seqable?` | Can produce a seq? (collections, strings, nil) |
| `ifn?` | Is invokable via direct call? (functions, keywords, maps, sets — NOT vectors). Note: maps/sets are invokable as `(my-map :key)` but cannot be passed directly to HOFs like `mapv`; wrap in a lambda: `(mapv #(my-map %) coll)` |
| `map-entry?` | Always false — no MapEntry type on BEAM |
| `type` | Returns the type as a keyword: `:boolean`, `:number`, `:string`, `:vector`, `:map`, `:set`, `:keyword`, `:regex`, `:function`. For `nil`, returns `nil` (not `:nil`). |

```clojure
;; coll? returns true for vectors, maps, and sets
(coll? [1 2 3])    ; => true
(coll? {:a 1})     ; => true
(coll? #{1 2})     ; => true
(coll? "hello")    ; => false
(coll? 42)         ; => false

;; sequential? returns true only for ordered collections (vectors)
(sequential? [1 2 3])  ; => true
(sequential? {:a 1})   ; => false
(sequential? #{1 2})   ; => false

;; seq? is effectively the same as sequential? (no lazy sequences in PTC-Lisp)
(seq? [1 2 3])     ; => true

;; Useful with tree-seq for walking nested vectors
(tree-seq sequential? seq [[1 2] [3 [4 5]]])
```

**Note:** Strings are not considered collections by any predicate. This affects functions like `flatten` which only flatten values where `coll?` is true.

**Collection Functions on Maps and Strings:**

Although maps and strings are not "collections" per `coll?`, many collection functions work on them:

| Function | Maps | Strings | Notes |
|----------|------|---------|-------|
| `count` | ✓ | ✓ | Returns key count / character count |
| `empty?` | ✓ | ✓ | True if no keys / no characters (or nil) |
| `not-empty` | ✓ | ✓ | Returns map/string if not empty, else nil |
| `first` | ✗ | ✓ | Maps: use `(first (keys m))`. Strings: returns first character |
| `second` | ✗ | ✓ | Maps: use `(second (keys m))`. Strings: returns second character |
| `last` | ✗ | ✓ | Maps: use `(last (keys m))`. Strings: returns last character |
| `nth` | ✗ | ✓ | Maps: not supported. Strings: returns character at index |
| `rest` | ✗ | ✓ | Strings: returns list of remaining characters |
| `butlast` | ✗ | ✓ | Strings: returns list of all but last character |
| `next` | ✗ | ✓ | Strings: returns list of remaining characters or nil |
| `take` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of first n characters |
| `drop` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of characters after dropping n |
| `take-last` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of last n characters |
| `drop-last` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of characters after dropping last n |
| `take-while` | ✓ | ✓ | Maps: iterates `[key value]` pairs. Strings: returns list of characters while predicate is true |
| `drop-while` | ✓ | ✓ | Maps: iterates `[key value]` pairs. Strings: returns list of characters after predicate becomes false |
| `map` | ✓ | ✓ | Maps: iterates over `[key value]` pairs. Strings: iterates over characters |
| `mapv` | ✓ | ✓ | Same as `map`, returns vector |
| `filter` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of characters |
| `remove` | ✓ | ✓ | Maps: returns list of `[key value]` pairs. Strings: returns list of characters |
| `find` | ✓ | ✗ | Maps: associative entry lookup. Strings: not associative and signal `:type_error` |
| `sort` | ✗ | ✓ | Strings: returns sorted list of characters |
| `sort-by` | ✓ | ✓ | Maps: returns sorted list of `[key value]` pairs. Strings: sorted list of characters |
| `reverse` | ✗ | ✓ | Strings: returns reversed list of characters |
| `distinct` | ✓ | ✓ | Maps: returns `[key value]` pairs (already unique). Strings: returns list of unique characters |
| `some` | ✗ | ✓ | Strings: returns first truthy result of predicate |
| `every?` | ✗ | ✓ | Strings: true if predicate is truthy for all characters |
| `not-any?` | ✗ | ✓ | Strings: true if predicate is false for all characters |
| `not-every?` | ✗ | ✓ | Strings: true if predicate is false for at least one character |
| `reduce` | ✓ | ✓ | Maps: iterates over `[key value]` pairs. Strings: iterates over characters |
| `entries` | ✓ | ✗ | Explicit conversion to list of `[key value]` pairs |

**Note:** String operations that return characters return lists of single-character strings, not a string. Use `(join "" result)` to convert back to a string if needed.

**Mapping over maps:** When you call `map` on a map, each entry is passed as a `[key value]` vector. Use destructuring to extract the key and value:

```clojure
;; Transform grouped data
(let [by-category (group-by :category expenses)]
  (map (fn [[cat items]]
         {:category cat :total (sum-by :amount items)})
       by-category))
```

To iterate over just keys or values, extract them first:
```clojure
(->> (keys my-map)
     (map (fn [k] {:key k :val (get my-map k)})))
```

### 8.8 Numeric Predicates

| Function | Description |
|----------|-------------|
| `zero?` | Is zero? |
| `pos?` | Is positive? |
| `neg?` | Is negative? |
| `even?` | Is even? |
| `odd?` | Is odd? |

**Note on Special Values:**
- `number?` returns `true` for `Infinity` and `NaN`.
- `pos?` returns `true` for `Double/POSITIVE_INFINITY`.
- `neg?` returns `true` for `Double/NEGATIVE_INFINITY`.
- All predicates (including `zero?`) return `false` for `Double/NaN`.
- `Double/NaN` is not equal to itself: `(= Double/NaN Double/NaN)` is `false`.

**Integer predicates on floats:** `even?` and `odd?` accept whole-number floats like `4.0` (treating them as integers), and return `false` for non-whole floats like `4.5`. This diverges from Clojure, which throws on float arguments (see [GAP-S08](clojure-conformance-gaps.md#gap-s08-even-odd-handle-floats-gracefully)).

```clojure
(even? 4)     ; => true
(even? 4.0)   ; => true  (whole-number float)
(even? 4.5)   ; => false (non-whole float)
```

### 8.9 String Parsing

| Function | Description |
|----------|-------------|
| `parse-long` | Parse string to integer, returns nil on failure |
| `parse-int` | Alias for `parse-long` |
| `parse-double` | Parse string to double, returns nil on failure |
| `parse-boolean` | Parse `"true"`/`"false"`, returns nil on failure |

String parsing functions provide safe conversion from strings to numbers, compatible with Clojure 1.11+. These functions return `nil` on parse failure rather than throwing exceptions.
Java-shaped numeric aliases are also accepted for LLM compatibility: `Integer/parseInt` and `Long/parseLong` map to `parse-long`, and `Double/parseDouble` and `Float/parseFloat` map to `parse-double`. These aliases keep PTC-Lisp's safe `nil`-on-failure behavior; they are not exact Java throwing semantics. `Boolean/parseBoolean` is a Java-compatible interop builtin: it returns `true` only for case-insensitive `"true"`, returns `false` for nil/null and every other string, and raises for non-string, non-nil inputs.

**Parsing behavior:**
- Both functions require the entire string to be consumed by the parse. Partial parses are rejected.
- Leading/trailing whitespace is not stripped—the string must be in exact numeric form.
- Invalid input returns `nil` rather than an error.

```clojure
;; Successful parses
(parse-long "42")          ; => 42
(parse-long "-17")         ; => -17
(parse-double "3.14")      ; => 3.14
(parse-double "-0.5")      ; => -0.5
(parse-double "1.23e-4")   ; => 1.23e-4
(Double/parseDouble "3.14") ; => 3.14
(Integer/parseInt "42")    ; => 42
(Boolean/parseBoolean "true") ; => true
(Boolean/parseBoolean "TRUE") ; => true
(Boolean/parseBoolean "x") ; => false
```

### 8.10 Regex Functions

Regex functions provide validation and extraction capabilities. To ensure system stability, PTC-Lisp uses a "Safety-First" regex engine with forced backtracking and recursion limits.

| Function | Signature | Description |
|----------|-----------|-------------|
| `re-pattern` | `(re-pattern s)` | Compile string `s` into an opaque regex object |
| `re-find` | `(re-find re s)` | Returns the first match of `re` in `s` |
| `re-matches` | `(re-matches re s)` | Returns match if `re` matches the **entire** string `s` |
| `re-seq` | `(re-seq re s)` | Returns all matches of `re` in `s` as a list |
| `re-split` | `(re-split re s)` | Split string `s` by regex pattern `re` |
| `regex?` | `(regex? x)` | Returns true if `x` is a regex object |
| `extract` | `(extract pattern s)` | Extract capture group 1 from match |
| `extract` | `(extract pattern s n)` | Extract capture group n (0 = full match) |
| `extract-int` | `(extract-int pattern s)` | Extract group 1 and parse as integer |
| `extract-int` | `(extract-int pattern s n)` | Extract group n and parse as integer |
| `extract-int` | `(extract-int pattern s n default)` | Extract group n, parse as int, return default on failure |

**Opaque Regex Type:** Regexes do not have a literal syntax. They must be created using `re-pattern`. Internally, they are opaque objects that can be passed to functions but not inspected directly.

**Return Value Semantics:**
- If no match is found, `re-find` and `re-matches` return `nil`; `re-seq` returns an empty list.
- If the regex has no capture groups, returns the matching string (or list of strings for `re-seq`).
- If the regex contains capture groups, returns a **vector** where the first element is the full match and subsequent elements are the groups.

```clojure
(re-find (re-pattern "\\d+") "v1")              ; => "1"
(re-matches (re-pattern "\\d+") "123")          ; => "123"
(re-matches (re-pattern "\\d+") "123abc")       ; => nil (not entire string)
(re-find (re-pattern "(\\d+)-(\\d+)") "10-20")  ; => ["10-20" "10" "20"]
(re-seq (re-pattern "\\d+") "a1b22c333")        ; => ["1" "22" "333"]
(re-seq (re-pattern "(\\d)(\\w)") "1a2b")       ; => [["1a" "1" "a"] ["2b" "2" "b"]]
(re-split (re-pattern "\\s+") "a  b   c")       ; => ["a" "b" "c"]
(re-split (re-pattern ",") "a,b,c")             ; => ["a" "b" "c"]

;; extract - simplified capture group extraction
(extract "ID:(\\d+)" "ID:42")                   ; => "42" (group 1)
(extract "ID:(\\d+)" "ID:42" 0)                 ; => "ID:42" (full match)
(extract "x=(\\d+) y=(\\d+)" "x=10 y=20" 2)     ; => "20" (group 2)
(extract "ID:(\\d+)" "no match")                ; => nil

;; extract-int - extract and parse as integer
(extract-int "age=(\\d+)" "age=25")             ; => 25
(extract-int "age=(\\d+)" "no match")           ; => nil (2-arity)
(extract-int "x=(\\d+) y=(\\d+)" "x=10 y=20" 2) ; => 20 (group 2)
(extract-int "age=(\\d+)" "no match" 1 0)       ; => 0 (4-arity with default)
(extract-int "x=(\\d+) y=(\\d+)" "x=10 y=20" 2 0) ; => 20 (group 2 with default)
```

**Note:** `#"..."` is shorthand for `(re-pattern "...")`. Both forms produce compiled regex values. `split` follows Clojure: the delimiter is a regex. A single-character string delimiter is accepted (chars are one-character strings), but a multi-character string delimiter signals a `:type_error` — use a regex literal for those, e.g. `(split s #"---\n")`. For splitting on newlines, prefer `(split-lines s)`.

**Safety Constraints:**
- **Match Limit:** Regex execution is restricted to 100,000 backtracking steps. Exceeding this limit (e.g., due to ReDoS) terminates evaluation with an error.
- **Input Truncation:** To prevent super-linear scaling on massive inputs, regex functions only scan the first 32KB of any input string.
- **Pattern Complexity:** Patterns are limited to 256 bytes in length.


```clojure
;; Failed parses

(parse-long "abc")         ; => nil
(parse-double "invalid")   ; => nil
(parse-long "42abc")       ; => nil (partial parse rejected - must consume entire string)
(parse-double "3.14 ")     ; => nil (trailing whitespace not allowed)
```

**Type checking:**
Both functions accept strings and return `nil` for non-string input (see [DIV-18](clojure-conformance-gaps.md#div-18-parse-long-parse-double-parse-boolean-return-nil-for-non-string-input)).

```clojure
(parse-long 42)            ; => ...
(parse-long nil)           ; => ...
(parse-double nil)         ; => ...
(parse-double 3.14)        ; => ...
```

**Use cases:**
Typical usage involves filtering valid parses from potentially invalid input:

```clojure
;; Extract valid integers from mixed data
(->> ["1" "2" "not-a-number" "4"]
     (map parse-long)
     (filter some?)
     (reduce + 0))  ; => 7
```

### 8.11 Function Combinators

| Function | Signature | Description |
|----------|-----------|-------------|
| `juxt` | `(juxt f1 f2 ...)` | Returns a function that applies all functions and returns a vector of results |
| `comp` | `(comp f1 f2 ...)` | Returns a function composing fns right-to-left; `(comp)` returns `identity` |
| `partial` | `(partial f arg1 ...)` | Returns a function with some arguments pre-filled |
| `complement` | `(complement f)` | Returns a function with the opposite truth value (always boolean) |
| `constantly` | `(constantly x)` | Returns a function that always returns `x`, ignoring its arguments |
| `every-pred` | `(every-pred p1 p2 ...)` | Returns a predicate true when all preds are satisfied (always boolean) |
| `some-fn` | `(some-fn f1 f2 ...)` | Returns a function that returns the first truthy result from any fn |

The `juxt` combinator creates a function that applies each of its argument functions to the same input and returns a vector containing all results. This is particularly useful for multi-criteria sorting and extracting multiple values at once. `juxt` requires at least one function — a zero-argument `(juxt)` is an arity error (GAP-S110), matching Clojure.

```clojure
;; Basic usage: extract multiple values from a map
((juxt :name :age) {:name "Alice" :age 30})
; => ["Alice" 30]

;; Multi-criteria sorting (primary: priority, secondary: name)
(sort-by (juxt :priority :name) tasks)
; Sorts first by priority, then by name for equal priorities

;; Extracting coordinates from point maps
(map (juxt :x :y) points)
; => [[1 2] [3 4] ...]

;; Using closures for computed values
((juxt #(+ % 1) #(* % 2)) 5)
; => [6 10]

;; Using builtin functions
((juxt first last) [1 2 3])
; => [1 3]
```

**Comparison with explicit function:**

```clojure
;; These are equivalent:
(sort-by (juxt :priority :name) tasks)
(sort-by (fn [t] [(:priority t) (:name t)]) tasks)

;; juxt is more concise for multiple key extraction
(map (juxt :id :name :email) users)
(map (fn [u] [(:id u) (:name u) (:email u)]) users)
```

**Supported function types:**
- Keywords (used as map accessors)
- Closures (`fn` and `#()` syntax)
- Builtin functions (`first`, `last`, `count`, etc.)

**`comp`** — function composition (right-to-left):

```clojure
((comp str inc) 5)            ; => "6" (inc first, then str)
((comp str +) 1 2 3)          ; => "6" (rightmost fn gets all args)
((comp inc inc inc) 0)        ; => 3
((comp) 42)                   ; => 42 (identity)
(map (comp inc inc) [1 2 3])  ; => [3 4 5]
```

**`partial`** — partially apply arguments:

```clojure
((partial + 10) 5)              ; => 15
((partial + 1 2))               ; => 3 (no extra args needed)
(map (partial + 10) [1 2 3])    ; => [11 12 13]
((partial str "a" "b") "c" "d") ; => "abcd"
```

**`complement`** — negate a predicate (returns boolean):

```clojure
((complement even?) 3)                 ; => true
((complement even?) 4)                 ; => false
(filter (complement even?) [1 2 3 4])  ; => [1 3]
```

**`constantly`** — ignore arguments, always return the same value:

```clojure
((constantly 5) 1 2 3)   ; => 5
((constantly nil) :a :b)  ; => nil
```

**`every-pred`** — combine predicates with AND (returns boolean):

```clojure
((every-pred even? pos?) 4)       ; => true
((every-pred even? pos?) -4)      ; => false (pos? fails)
((every-pred even? pos?) 4 6 8)   ; => true (all values pass all preds)
(filter (every-pred even? pos?) [-2 -1 0 1 2 3 4])  ; => [2 4]
```

**`some-fn`** — combine functions with OR (returns actual truthy value):

```clojure
((some-fn :a :b) {:a 1})   ; => 1 (returns the value, not true)
((some-fn :a :b) {:b 2})   ; => 2
((some-fn :a :b) {:c 3})   ; => nil (no match)
((some-fn even? pos?) 3)   ; => true (pos? matches)
```

### 8.12 Functional Tools: apply

| Function | Signature | Description |
|----------|-----------|-------------|
| `apply` | `(apply f coll)` | Applies function `f` to the argument sequence `coll` |
| | `(apply f x y ... coll)` | Applies function `f` to `x`, `y`, ... and the argument sequence `coll` |

The `apply` function invokes a function `f` with the provided arguments. The last argument must be a collection (vector or set), which is "unrolled" into individual arguments. Any arguments between `f` and the collection are passed as fixed prefix arguments.

```clojure
;; Basic usage
(apply + [1 2 3])              ; => 6
(apply str ["a" "b" "c"])      ; => "abc"

;; Spreading with fixed arguments
(apply + 1 2 [3 4])            ; => 10
(apply merge {:a 1} [{:b 2} {:c 3}]) ; => {:a 1 :b 2 :c 3}

;; With Keywords as functions
(apply :name [{:name "Alice"}]) ; => "Alice"

;; With Sets as functions
(apply #{1 2 3} [2])           ; => 2

;; With filtering (passing apply as a value)
(map #(apply + %) [[1 2] [3 4]]) ; => [3 7]
```

**Edge Cases:**
- **Empty collection:** `(apply + [])` is equivalent to `(+)`, returning `0`.
- **Nil as last argument:** `(apply + 1 2 nil)` returns a `type-error`. PTC-Lisp requires an explicit collection.
- **Sets as last argument:** `(apply + 1 #{2 3})` is allowed, but since sets are unordered, the application order is undefined (not an issue for commutative operations like `+`).
- **Non-callable first argument:** Raises a `not-callable` error.
- **Non-collection last argument:** Raises a `type_error`.

### 8.13 Debugging with println

| Function | Signature | Description |
|----------|-----------|-------------|
| `println` | `(println ...)` | Prints arguments to the execution trace, separated by spaces. Returns `nil`. |

The `println` function is the **only way to inspect values** during multi-turn SubAgent execution. Expression results are NOT shown to the LLM — only explicit `println` output appears in feedback.

**Behavior:**
- Arguments are converted to Clojure syntax strings.
- Multiple arguments are separated by single spaces.
- Each `println` call results in a new line in the output buffer.
- Returns `nil`.

```clojure
(def results (tool/search {:q "test"}))
(println "Found:" (count results))      ; shown in feedback
(println "First:" (first results))      ; shown in feedback
results                                  ; NOT shown - use println to inspect
```

**Multi-Turn Feedback:**
In SubAgent multi-turn loops, the LLM only sees:
1. `println` output from the current turn
2. Stored symbol names (from `def`)
3. Turn information

Expression results are intentionally hidden to encourage explicit inspection and reduce token waste.

**Trace Output:**
Programs that call `println` will have their output available in the `prints` list of the result:

```elixir
# Result of Lisp.run(...)
{:ok, %Step{
  return: [...],
  prints: ["Found: 42", "First: {:id 1}"]
}}
```

**Note:** In parallel operations like `pmap` and `pcalls`, `println` output from parallel branches is not captured. This is intentional—parallel branches communicate via return values, not side effects. Use `println` for sequential debugging between turns.

### 8.14 Date and Time (Minimal Java Interop)

PTC-Lisp supports a minimal subset of Java interop for date and time handling, simulating bounded behavior from `java.util.Date`, `java.time.LocalDate`, `java.time.Instant`, `java.time.Duration`, and `java.lang.System`.

| Symbol | Signature | Description |
|--------|-----------|-------------|
| `java.util.Date.` | `(java.util.Date.)` | Current UTC time |
| | `(java.util.Date. arg)` | Construct from timestamp (ms/sec), ISO-8601/RFC 2822 string, or an existing DateTime/NaiveDateTime/Date |
| `java.time.LocalDate/parse` | `(java.time.LocalDate/parse s)` | Parse ISO-8601 date string (`YYYY-MM-DD`) into a Date; if the string contains a time component (`...T...`), returns a DateTime instead |
| `Instant/parse` | `(Instant/parse iso-string)` | Parse an ISO-8601 instant/date-time string to a DateTime (offsetless `...T...` strings treated as UTC; bare `YYYY-MM-DD` returns a Date instead); also available as `(parse iso-string)` |
| `parse` | `(parse iso-string)` | Alias for `Instant/parse` / `LocalDate/parse` auto-dispatch — same as `(Instant/parse ...)` |
| `.getTime` | `(.getTime date)` | Return Unix timestamp in milliseconds (**DateTime only** — works on results from `java.util.Date.`, `Instant/parse`, and `LocalDate/parse` when the input had a time component; does NOT work on bare Date objects, e.g. `LocalDate/parse` of a `YYYY-MM-DD` string) |
| `.toEpochDay` | `(.toEpochDay local-date)` | Return a LocalDate's epoch-day integer (`1970-01-01` is `0`) |
| `.plusDays` | `(.plusDays local-date n)` | Add integer days to a LocalDate |
| `.minusDays` | `(.minusDays local-date n)` | Subtract integer days from a LocalDate |
| `Duration/between` | `(Duration/between start end)` | Return a Duration between two DateTime instants; also available as `(java.time.Duration/between start end)` |
| `.toMillis` | `(.toMillis duration)` | Return a Duration length in milliseconds |
| `.toDays` | `(.toDays duration)` | Return a Duration length in whole days, truncating partial days toward zero |
| `.isBefore` | `(.isBefore a b)` | Returns true if `a` comes strictly before `b` (same-type only) |
| `.isAfter` | `(.isAfter a b)` | Returns true if `a` comes strictly after `b` (same-type only) |
| `System/currentTimeMillis` | `(System/currentTimeMillis)` | Return current Unix milliseconds |

#### Constructor `java.util.Date.`

- **No arguments**: Returns a `DateTime` object for the current UTC time.
- **Integer argument**: Smart unit detection.
    - If `abs(ts) < 1,000,000,000,000`: Treated as **Unix seconds**.
    - Otherwise: Treated as **Unix milliseconds**.
- **String argument**: Attempts to parse in the following order:
    1. **ISO-8601 with offset** (e.g., `"2026-01-08T14:30:00Z"`)
    2. **ISO-8601 without offset** (e.g., `"2026-01-08T14:30:00"` — what `(str ~N[...])` produces; treated as UTC)
    3. **Date-only ISO** (e.g., `"2026-01-08"`, defaults to midnight UTC)
    4. **RFC 2822** (e.g., `"Wed, 8 Jan 2026 14:30:00 +0000"`, common in email headers)
- **Temporal struct argument**: Pass-through for `DateTime`; `Date` and `NaiveDateTime` upgrade to UTC (midnight for Date). `Time` alone raises (no date component). Lets generated programs write `(java.util.Date. (:opened_at t))` directly when a tool returns Elixir temporal values.

#### `java.time.LocalDate/parse`

- **Shorthand**: `(LocalDate/parse s)` is also supported; also available as the bare `(parse s)` builtin.
- **Behavior**: Auto-dispatches on the input string:
  - `YYYY-MM-DD` (date-only): Returns an opaque **Date** object representing just the date (no time).
  - `...T...` (string containing a time component): Returns a **DateTime** instead (divergence from Java's strict `LocalDate.parse`; see `Instant/parse`).
- **Format**: When displayed or returned to an LLM, a Date is formatted as an ISO string: `"2023-10-27"`.

#### `Instant/parse`

- **Shorthand**: `(Instant/parse s)` and the bare `(parse s)` are both supported (all three share the same auto-dispatch implementation).
- **Behavior**: Parses an ISO-8601 instant or date-time string to a **DateTime**:
  - Accepts an explicit offset (`Z`, `+02:00`, …).
  - An offsetless `...T...` string (e.g., `"2026-01-08T14:30:00"`) is treated as UTC.
  - A bare `YYYY-MM-DD` string (no time component) returns a **Date** instead (same as `LocalDate/parse`).
- **Methods**: `.isBefore`, `.isAfter`, and `.getTime` all work on the returned DateTime.

#### LocalDate Methods

- **`.toEpochDay`**: Takes a **Date** object and returns the number of days since `1970-01-01`.
- **`.plusDays` / `.minusDays`**: Take a **Date** object and an integer day count, returning a shifted **Date**.
- These methods do not work on DateTime values. Use `Duration/between` for instant differences.

#### `Duration/between`

- **Shorthand**: `(Duration/between start end)` and `(java.time.Duration/between start end)` are both supported.
- **Behavior**: Takes two **DateTime** values and returns an opaque **Duration** object representing `end - start`.
- **Methods**:
  - `(.toMillis duration)` returns the signed millisecond difference.
  - `(.toDays duration)` returns signed whole days; partial days truncate toward zero, including negative partial days.
- **Display**: When displayed or returned to an LLM, a Duration is formatted as `#duration[<milliseconds>ms]`.
- **No bare alias**: `(between start end)` is not supported.
- **LocalDate differences**: Use `(.toEpochDay date)` and subtraction for total day differences between LocalDate values.

#### Methods and Utilities

- **`.getTime`**: Takes a **DateTime** object and returns its value as Unix milliseconds (integer). Works on results from `java.util.Date.`, `Instant/parse`, and `LocalDate/parse` when the input contained a time component. **Does NOT work on Date objects** (bare `YYYY-MM-DD` results).
- **`.toEpochDay`**: Takes a **Date** object and returns its epoch-day integer. This is the preferred way to compute total LocalDate day differences: `(- (.toEpochDay b) (.toEpochDay a))`.
- **`.toMillis` / `.toDays`**: Take a **Duration** object returned by `Duration/between`.
- **`System/currentTimeMillis`**: Returns the current system time in milliseconds.

#### Errors and Type Safety
- Passing `nil` to `java.util.Date.`, `LocalDate/parse`, or `.getTime` raises an error.
- Invalid strings or types raise descriptive errors.
- Java-shaped namespace membership is bounded. For example, `Duration/between` is supported but bare `(between ...)` and unrelated members such as `LocalDate/currentTimeMillis` are not.
- **Unsupported Methods**: Calling unregistered dot-methods (e.g., `(.toString date)`) provides a hint listing supported interop functions.

#### Comparison

Date and DateTime objects support `.isBefore` and `.isAfter` for direct comparison:
```clojure
(.isBefore (LocalDate/parse "2023-01-01") (LocalDate/parse "2023-12-31"))  ; => true
(.isAfter (java.util.Date. "2024-01-01T00:00:00Z") (java.util.Date. "2023-01-01T00:00:00Z"))  ; => true
```

Both arguments must be the same type. Comparing a `LocalDate` with a `DateTime` raises an error:
```clojure
(.isBefore (LocalDate/parse "2023-01-01") (java.util.Date. "2023-01-01T00:00:00Z"))
; => Error: cannot compare LocalDate with DateTime — use same types
```

**Legacy alternatives** (still work, but `.isBefore`/`.isAfter` are preferred):
```clojure
(< (.getTime d1) (.getTime d2))  ; DateTime millisecond comparison
(< (str d1) (str d2))            ; LocalDate lexicographic comparison (ISO-8601)
```

### 8.15 String Methods (Java Interop)

PTC-Lisp supports Java-style string methods for common operations.

| Method | Signature | Description |
|--------|-----------|-------------|
| `.indexOf` | `(.indexOf s substr)` | Index of first occurrence, or -1 if not found |
| `.indexOf` | `(.indexOf s substr from)` | Index of first occurrence starting from position |
| `.lastIndexOf` | `(.lastIndexOf s substr)` | Index of last occurrence, or -1 if not found |
| `.toLowerCase` | `(.toLowerCase s)` | Convert string to lower case |
| `.toUpperCase` | `(.toUpperCase s)` | Convert string to upper case |
| `.startsWith` | `(.startsWith s prefix)` | Returns true if string starts with prefix |
| `.endsWith` | `(.endsWith s suffix)` | Returns true if string ends with suffix |
| `.contains` | `(.contains s substr)` | Returns true if string contains substring; raises on non-string |
| `.length` | `(.length s)` | Grapheme count of string; raises on non-string |
| `.substring` | `(.substring s start)` | Suffix from grapheme index `start`; raises on out-of-range index |
| `.substring` | `(.substring s start end)` | Graphemes in `[start, end)`; raises on out-of-range index |

```clojure
(.indexOf "hello" "ll")      ; => 2
(.indexOf "hello" "x")       ; => -1
(.indexOf "hello" "l" 3)     ; => 3 (finds second 'l')
(.lastIndexOf "hello" "l")   ; => 3 (last 'l')
(.toLowerCase "Hello")       ; => "hello"
(.toUpperCase "Hello")       ; => "HELLO"
(.startsWith "hello" "he")   ; => true
(.endsWith "hello" "lo")     ; => true
(.contains "hello" "ell")    ; => true
(.length "hello")            ; => 5
(.substring "hello" 2)       ; => "llo"
(.substring "hello" 1 3)     ; => "el"
```

Unlike the Clojure-named string functions (which return signal values), these Java-named methods **raise** on bad input — `.substring` raises when `start < 0`, `start > length`, `end > length`, or `start > end`.

**Return Value**: These methods return -1 when the substring is not found (Java semantics). **Prefer `index-of` / `last-index-of`** (§8.3) which return `nil` when not found (Clojure semantics).

**Errors**: Passing a non-string raises a descriptive error:
```clojure
(.indexOf 123 "x")  ; => error: .indexOf: expected string, got integer
```

---

---

## 9. Namespaces, Context, and Tools

Programs have access to data and functions through **namespaced symbols** and **special forms**.

### 9.1 Namespace Overview

| Access Pattern | Source | Description |
|----------------|--------|-------------|
| Plain symbols | Stored values | Values defined via `def`/`defn`, persisted across turns |
| `data/` | Current request context | Current request context (read-only) |
| `tool/` | Tool invocation | Call registered tools |
| `budget/` | Budget introspection | Query remaining budget (turns, tokens, depth) |
| `tool/servers` | REPL discovery | List configured MCP upstream servers (requires a discovery backend) |
| `apropos`, `dir`, `doc`, `meta`, `ns-publics` | REPL discovery | Inspect local PTC-Lisp builtins (and MCP tools when a discovery backend is configured) |
| `*1`, `*2`, `*3` | Recent results | Previous turn results (for debugging) |

### 9.2 Persistent Values — User Namespace symbols

Access values stored in the User Namespace as plain symbols. These values are defined using the `def` or `defn` forms and persist across turns within a session:

```clojure
high-paid          ; access symbol defined via (def high-paid ...)
orders             ; access symbol defined via (def orders ...)
query-count        ; access symbol defined via (def query-count ...)
```

Stored values are **read-only during evaluation** unless redefined via `def`. To update a value for the next turn, use `def` in your program (see Section 16).

```clojure
(def new-orders (tool/get-orders {:since "2024-01-01"}))
(def orders (concat orders new-orders))
orders ; return current total
```

With default values (using `or`):

```clojure
(def current-count (or query-count 0))
(def query-count (inc current-count))
query-count
```


### 9.3 Context Access — `data/`

Read from current request context using the `data/` namespace prefix:

```clojure
data/input                ; get :input from context
data/user-id              ; get :user-id from context
data/request-id           ; get :request-id from context
```

Context is **per-request** data passed by the host. It does not persist across turns.

```clojure
(->> data/expenses
     (filter (fn [e] (= (:category e) "travel")))
     (sum-by :amount))
```

### 9.4 Turn History — `*1`, `*2`, `*3`

Access results from previous turns using the turn history symbols:

```clojure
*1                        ; result from the previous turn (most recent)
*2                        ; result from 2 turns ago
*3                        ; result from 3 turns ago
```

**Semantics:**
- `*1` returns the result of the most recent turn
- Returns `nil` if the turn doesn't exist (e.g., `*1` on turn 1)
- The host controls how many prior return values are supplied. The public
  `PtcRunner.Session` wrapper keeps the last 3 successful returns by default.
- Use stored values (plain symbols defined via `def`) for persistent access to full values

**Use cases:**
- Quick inspection of previous results during debugging
- Lightweight chaining when full values aren't needed

```clojure
;; On turn 2, check if previous result was a list
(if (list? *1)
  (count *1)
  0)

;; Compare current with previous
(> (count data/items) (count *1))
```

**For reliable multi-turn patterns**, use `(def name value)` to store values in the User Namespace. Turn history (`*1`, `*2`, `*3`) is primarily a debugging aid, not a storage mechanism.

### 9.5 Budget Introspection — `budget/remaining`

Query the remaining budget using the `budget/` namespace. This enables programs to make intelligent decisions based on available resources.

```clojure
(budget/remaining)            ; returns budget map
```

**Return value:**

The `budget/remaining` primitive returns a map with hyphenated keys (Clojure-style):

```clojure
{:turns 15                    ; total turns remaining
 :work-turns 10               ; work turns remaining
 :retry-turns 5               ; retry turns remaining
 :depth {:current 1 :max 3}   ; nesting depth info
 :tokens {:input 5000         ; input tokens used
          :output 2000        ; output tokens used
          :total 7000         ; total tokens used
          :cache-creation 1000
          :cache-read 2000}
 :llm-requests 3}             ; LLM API calls made
```

**Note:** The `:turns`, `:work-turns`, and `:retry-turns` fields report **remaining** budget, whereas the `:tokens` fields report **accumulated usage** so far (not remaining).

**Accessing hyphenated keys:**

```clojure
(:work-turns (budget/remaining))              ; => 10
(:retry-turns (budget/remaining))             ; => 5
(:cache-read (:tokens (budget/remaining)))    ; => 2000
```

**Use cases:**

- Adjust processing strategy based on remaining turns
- Batch operations when budget is low
- Log resource usage for monitoring

```clojure
;; Choose strategy based on remaining budget
(let [b (budget/remaining)
      items data/items]
  (if (< (:turns b) (count items))
    ;; Low budget: batch process
    (tool/batch-process {:items items})
    ;; High budget: process individually
    (mapv (fn [i] (tool/process {:item i})) items)))
```

**Note:** Returns an empty map `{}` when running outside a SubAgent context (e.g., standalone `Lisp.run/2` without budget option).

### 9.6 Tool Invocation — `tool/tool-name`

Invoke registered tools using the `tool/` namespace:

```clojure
(tool/tool-name)                   ; no arguments
(tool/tool-name args-map)          ; with arguments (named parameters)
```

**Syntax:**
- Tool names become atoms in `tool/` namespace: `tool/tool-name`
- **Tools require named arguments** (maps):
  - No arguments: `(tool/get-users)` → tool receives `%{}`
  - Map argument: `(tool/fetch {:id 123})` → tool receives `%{"id" => 123}`
  - Keyword-style: `(tool/search :query "x" :limit 10)` → tool receives `%{"query" => "x", "limit" => 10}`

**Examples:**
```clojure
(tool/get-users)                   ; no arguments
(tool/search {:query "budget"})    ; single map argument
(tool/fetch {:id 123})             ; with parameters
(tool/search {:query "foo" :limit 10})

;; Store tool result for later use
(let [users (tool/get-users)]
  (->> users
       (filter :active)
       (count)))
```

**Tool boundary contract:**
- PTC-Lisp keywords (`:foo`) become **string keys** at the Elixir boundary
- Tools always receive string-keyed maps: `%{"key" => value}`
- This matches JSON conventions and prevents atom memory leaks
- Tool authors pattern match on string keys: `def run(%{"query" => q}, _ctx)`

**Tool behavior:**
- Tools are Elixir functions registered by the host
- Tools may have side effects (external API calls, database queries)
- Tool errors propagate as execution errors
- Tool calls are logged for auditing

### 9.7 REPL Discovery

Programs can inspect executable PTC-Lisp capabilities through REPL-style discovery forms. Plain `Lisp.run/2` exposes local PTC/Clojure builtins and curated Java interop. When PtcRunner runs as an MCP aggregator (`ptc_runner_mcp` with configured upstreams), the same forms also inspect configured upstream MCP tools. `tool/servers` remains MCP-only and requires a configured discovery backend. See [docs/aggregator-mode.md](aggregator-mode.md#repl-discovery-from-ptc-lisp) for the aggregator details.

| Form | Signature | Returns |
|------|-----------|---------|
| `tool/servers` | `(tool/servers)` | List of `{"name" "description" "tool_count" "catalog_loaded"}` maps. Raises a runtime error if no discovery backend is configured (this is neither a world fault nor a recoverable programmer fault). |
| `apropos` | `(apropos query)` / `(apropos query opts)` | Deterministic lexical search returning compact strings for executable local refs and MCP tools. MCP loaded tools rank before MCP unloaded-server hints, which rank before local results. `opts`: `:limit` (1..50, default 8) and `:load` (boolean, default false). |
| `dir` | `(dir ref)` / `(dir ref opts)` | List of members for a local namespace/curated Java class, or tools for one MCP server. `opts`: `:limit` (1..200, default 50) and `:offset` (≥ 0, default 0). |
| `doc` | `(doc ref)` | Detailed documentation for an executable local ref or MCP tool. Known local refs win; unknown refs fall through to MCP when available. |
| `meta` | `(meta ref)` | Structured metadata for an executable local ref or MCP tool. Known local refs win; unknown refs fall through to MCP when available. |
| `ns-publics` | `(ns-publics ns)` | Map of public names to compact metadata for local PTC/Clojure namespaces only. Java classes and MCP servers are not supported. |

Discovery only reports executable PTC-Lisp capabilities. For Java-shaped compatibility aliases, discovery returns executable refs such as `Integer/parseInt`, `System/currentTimeMillis`, `Math/abs`, and `java.time.LocalDate/parse`; it does not advertise unsupported fully-qualified `java.lang.*` call forms.

**MCP error model** (same split as `tool/call`):
- *World faults* — an upstream that can't be started, an oversized discovery result, or an exhausted per-program discovery budget — make the form return `nil`. The program continues.
- *Programmer faults* — an unknown server name, an unknown tool name, or a bad argument (e.g. `:limit` out of range) — raise an execution error that terminates the program.

```clojure
;; Discover which upstreams are available, then describe a tool on one of them
(let [servers (tool/servers)]
  (when (some (fn [s] (= (:name s) "github")) servers)
    (doc 'github/search_repos)))

;; Find tools related to "read" across every configured upstream
(apropos "read")
```

The discovery op budget is separate from the `tool/call` budget; discovery never consumes upstream-call quota.

### 9.8 Namespace Compatibility

LLMs often generate code with namespace-qualified symbols. PTC-Lisp does not
evaluate namespace declarations (`ns`, `require`, `refer`, `import`), but it
does allow a fixed set of namespace-qualified symbols. These normalize to
built-ins or reserved runtime operations at analysis time.

**Supported namespaces:**

| Group | Namespace(s) | Category |
|-------|--------------|----------|
| Clojure compatibility | `clojure.core`, `core` | Core functions |
| Clojure compatibility | `clojure.string`, `str`, `string` | String functions |
| Clojure compatibility | `clojure.set`, `set` | Set functions |
| Clojure compatibility | `clojure.walk`, `walk` | Tree traversal functions |
| Clojure compatibility | `regex` | Regex helpers (`re-find`, `re-pattern`, etc.; underlying vars are audited as `clojure.core`) |
| Java compatibility | `Math` | Math functions |
| Java compatibility | `System` | Java System time helper |
| Java compatibility | `Boolean` | Boolean parse alias |
| Java compatibility | `Double` | Double constants and parse alias |
| Java compatibility | `Float` | Float parse alias (returns PTC-Lisp double/float) |
| Java compatibility | `Integer`, `Long` | Integer parse aliases |
| Java compatibility | `LocalDate`, `java.time.LocalDate` | Java Date parsing (ISO-8601) |
| Java compatibility | `Instant`, `java.time.Instant` | Java Instant parsing (ISO-8601) |
| Java compatibility | `Duration`, `java.time.Duration` | Java Duration between helper |
| Java compatibility | `java.util.Date.` | Java Date constructors |
| PTC runtime/helper | `data` | Context access |
| PTC runtime/helper | `tool` | Registered tool invocation |
| PTC runtime/helper | `budget` | Remaining-budget introspection |
| PTC runtime/helper | `json` | JSON parse/generate helpers |
| MCP server extension | `mcp` | MCP server namespace. The `tool/servers` form is parsed unconditionally but requires a configured discovery backend at runtime (raises if none is set); there is no profile gate. |

**Examples of normalization:**

```clojure
;; These all normalize to the same built-in function:
(clojure.string/join "," items)   ; → (join "," items)
(str/join "," items)               ; → (join "," items)
(join "," items)                   ; (no change)

;; Core functions work too:
(clojure.core/map inc xs)          ; → (map inc xs)
(core/filter even? xs)             ; → (filter even? xs)

;; Tree traversal functions work via clojure.walk:
(clojure.walk/prewalk f data)      ; → (prewalk f data)
(walk/postwalk f data)             ; → (postwalk f data)

;; Regex helpers can be qualified when that improves clarity:
(regex/re-find #"error" line)      ; → (re-find #"error" line)

;; Java compatibility namespaces:
(Math/sqrt 9)                      ; → (sqrt 9)
(System/currentTimeMillis)         ; → (currentTimeMillis)
(Instant/parse "2026-05-18T12:00:00Z") ; → (parse "2026-05-18T12:00:00Z")
```

**Error handling:**

When a namespaced function doesn't exist as a built-in, the analyzer provides helpful error messages with available alternatives:

```clojure
(clojure.string/capitalize s)
;; Error: capitalize is not available. String functions: str, subs, join, split, trim, ...

(clojure.set/project relations [:id])
;; Error: project is not available. Set functions: set, set?, vec, vector, contains?, intersection, union, difference

(clojure.walk/stringify-keys data)
;; Error: stringify-keys is not available. Walk functions: prewalk, postwalk, walk
```

**Note:** The `data/` and `tool/` namespaces are reserved for context access
and tool invocation respectively. They are not aliases for Clojure
namespaces.

---

## 10. Complete Examples

### 10.1 Filter and Sum (Pure Query)

Filter expenses by category and sum amounts:

```clojure
(->> data/expenses
     (filter (fn [e] (= (:category e) "travel")))
     (sum-by :amount))
```

Returns a number. No memory update (non-map result).

### 10.2 Find Single Item

Find the cheapest product:

```clojure
(min-by :price data/products)
```

Find employee with most years:

```clojure
(max-by :years-employed data/employees)
```

### 10.3 Sort and Limit

Get top 5 products by price:

```clojure
(->> data/products
     (sort-by :price >)
     (take 5))
```

### 10.4 Extract Field Values

Get all product names:

```clojure
(map :name data/products)
```

### 10.5 Conditional Classification

Classify invoice by total:

```clojure
(let [{:keys [total]} data/invoice]
  (cond
    (> total 1000) "high-value"
    (> total 100)  "medium-value"
    :else          "low-value"))
```

### 10.6 Complex Filtering

Find eligible orders (high value, premium status, not flagged):

```clojure
(->> data/orders
     (filter (fn [o]
               (and (> (:total o) 100)
                    (or (= (:status o) "vip")
                        (= (:status o) "premium"))
                    (not (:flagged o))))))
```

### 10.7 Transform and Select Fields

Get names and emails of active users:

```clojure
(->> data/users
     (filter :active)
     (mapv (fn [u] (select-keys u [:name :email]))))
```

### 10.8 Combine Multiple Data Sources

Join orders with user information:

```clojure
(let [users (tool/get-users)
      orders (tool/get-orders)]
  (->> orders
       (filter (fn [o] (> (:total o) 100)))
       (mapv (fn [order]
               (let [user (first (filter (fn [u] (= (:id u) (:user-id order))) users))]
                 (merge order (select-keys user [:name :email])))))))
```

### 10.9 Grouping and Aggregation

Sum expenses by category:

```clojure
(let [by-category (group-by :category data/expenses)]
  (->> (keys by-category)
       (mapv (fn [cat]
               {:category cat
                :total (sum-by :amount (get by-category cat))}))))
```

### 10.10 Nested Data Access

Get email from nested user profile:

```clojure
(get-in data/user [:profile :contact :email])
```

Filter by nested field:

```clojure
(->> data/users
     (filter (fn [u] (= (get-in u [:profile :verified]) true))))
```

---

## 11. Semantics and Edge Cases

### 11.1 Empty Collections

| Operation | Empty Input | Result |
|-----------|-------------|--------|
| `(count [])` | `[]` | `0` |
| `(first [])` | `[]` | `nil` |
| `(last [])` | `[]` | `nil` |
| `(sum [])` | `[]` | `0` |
| `(avg [])` | `[]` | `nil` |
| `(sum-by :x [])` | `[]` | `0` |
| `(avg-by :x [])` | `[]` | `nil` |
| `(min-by :x [])` | `[]` | `nil` |
| `(max-by :x [])` | `[]` | `nil` |
| `(distinct-by :x [])` | `[]` | `[]` |
| `(filter pred [])` | `[]` | `[]` |
| `(sort-by :x [])` | `[]` | `[]` |

### 11.2 Nil Handling

```clojure
;; Accessing missing key returns nil
(get {:a 1} :b)              ; => nil
(:b {:a 1})                  ; => nil
(get-in {:a {:b 1}} [:a :c]) ; => nil

;; Arithmetic with nil is a type error
(+ 1 nil)                    ; => TYPE ERROR

;; Equality with nil is allowed
(= nil nil)                  ; => true
(= 5 nil)                    ; => false
(nil? nil)                   ; => true

;; Ordering comparisons with nil are recoverable term-order predicates
(> 5 nil)                    ; => false
(< nil 10)                   ; => false

;; filter/map handle nil gracefully
(filter (fn [m] (= (:x m) nil)) [{:x nil} {:x 1}])  ; => [{:x nil}]
```

### 11.3 Recoverable Ordered Comparisons

Ordering comparisons (`>`, `<`, `>=`, `<=`) are recoverable in PTC-Lisp. They return booleans rather than raising for `nil`, strings, keywords, maps, and mixed scalar values. For non-NaN values they use the runtime's total term ordering; `NaN` comparisons return false.

```clojure
;; Numeric comparisons
(> 5 3)                      ; => true
(< 1.5 2.0)                  ; => true

;; Recoverable term-order comparisons
(> "b" "a")                  ; => true
(< 1 nil)                    ; => true
(<= nil nil)                 ; => true
(< Double/NaN 0.0)           ; => false
```

**Note on sorting:** The `sort` and `sort-by` functions use related internal comparison behavior that supports both numbers and strings:

```clojure
(sort ["b" "a" "c"])         ; => ["a" "b" "c"]
(sort-by :name users)        ; sorts alphabetically
```

### 11.4 Aggregation with Missing/Nil Fields

```clojure
;; sum-by skips nil/missing fields
(sum-by :amount [{:amount 10} {:amount nil} {:other 5}])  ; => 10

;; avg-by skips nil/missing (not counted in denominator)
(avg-by :amount [{:amount 10} {:amount nil} {:amount 20}])  ; => 15.0

;; min-by/max-by skip nil values
(min-by :price [{:price nil} {:price 10} {:price 5}])  ; => {:price 5}
```

### 11.5 Non-Numeric Aggregation Fields

Aggregation functions require numeric field values:

```clojure
;; Arithmetic error - string in numeric aggregation
(sum-by :amount [{:amount "10"} {:amount 20}])  ; => ARITHMETIC ERROR

;; Arithmetic error - map in numeric aggregation
(avg-by :value [{:value {:x 1}}])              ; => ARITHMETIC ERROR
```

**Rule:** If a field exists and is not `nil` but is non-numeric, aggregation functions raise an arithmetic error (`:arithmetic_error`). Only `nil` and missing fields are silently skipped.

### 11.6 Short-Circuit Evaluation

`and` and `or` short-circuit:

```clojure
(and false (tool/expensive))  ; "expensive" not called
(or true (tool/expensive))   ; "expensive" not called
```

### 11.7 Keyword as Function with Default

```clojure
(:name {:name "Alice"})           ; => "Alice"
(:name {})                        ; => nil
(:name {} "Unknown")              ; => "Unknown"
```

### 11.8 Map as Function

Maps can be called as functions with a keyword argument:

```clojure
({:name "Alice"} :name)           ; => "Alice"
({} :name)                        ; => nil
({} :name "Unknown")              ; => "Unknown"
```

### 11.9 Flatten Behavior

`flatten` recursively flattens nested collections:

```clojure
(flatten [[1 2] [3 [4]]])         ; => [1 2 3 4]
(flatten [1 [2 {:a 3}] "str"])    ; => [1 2 {:a 3} "str"]
```

- Only vectors (Erlang lists) are flattened — `flatten` delegates to `List.flatten/1`, which recurses only into lists
- Maps, sets, strings, and other non-list values pass through unchanged (even though sets satisfy `coll?`, they are not flattened)

### 11.10 Tool Call Evaluation Order

Tool calls are evaluated in left-to-right order and never reordered:

```clojure
(let [a (tool/tool-1)   ; called first
      b (tool/tool-2)]  ; called second
  [a b])
```

This matters because tools may have side effects. The interpreter guarantees:
- Arguments evaluated left-to-right
- Tool calls execute in program order
- No speculative or parallel execution

---

## 12. Error Handling

Errors are represented as tagged tuples: `{:error, {error_type, details}}`. The error type is an atom, and details vary by error type (usually a message string, but may include additional context like expected/got values for type errors). Examples:

```elixir
{:error, {:parse_error, "unexpected token at line 3"}}
{:error, {:invalid_form, "let bindings require even number of forms"}}
{:error, {:type_error, "expected number, got string", [nil]}}  # 3rd element is the arg list / bad value
{:error, {:runtime_error, "runtime evaluation failed"}}
{:error, {:tool_error, "get-users", "connection refused"}}
{:error, {:timeout, 5000}}
{:error, {:memory_exceeded, 10_000_000}}
```

The formatted strings shown below are human-readable renderings for display to users or LLMs.

### 12.1 Error Types

| Error Type (atom) | Cause |
|------------|-------|
| `:parse_error` | Invalid syntax |
| `:invalid_form` | Malformed program structure (static analysis phase) |
| `:invalid_arity` | Wrong number of arguments to a special form (detected during analysis) |
| `:type_error` | Wrong argument type |
| `:arithmetic_error` | Arithmetic operation error (e.g. integer division by zero) |
| `:arity_error` / `:arity_mismatch` | Wrong number of arguments / closure arity mismatch (detected at runtime) |
| `:unbound_var` | Unknown symbol/variable |
| `:not_callable` | Attempt to call a non-callable value |
| `:runtime_error` | General runtime evaluation error |
| `:loop_limit_exceeded` | `loop`/`recur` iteration limit exceeded |
| `:unknown_tool` | Tool not registered |
| `:tool_error` | Tool execution failed |
| `:destructure_error` | Destructuring pattern mismatch |
| `:invalid_placeholder` | Invalid placeholder in `#()` syntax |
| `:unsupported_pattern` | Unsupported destructuring/binding pattern |
| `:unsupported_method` | Unknown Java-interop method |
| `:timeout` | Execution time exceeded |
| `:memory_exceeded` | Memory limit exceeded |

### 12.2 Error Message Format

Errors should include location and context when available. Source location tracking (line/column) is recommended but optional for v1 implementations—at minimum, errors must include the error type and a descriptive message.

```
type-error at line 5:
  (sum-by :amount items)

  'sum-by' expected a collection, got string: "not a list"

  Context: items was bound at line 2:
    (let [items data/data] ...)
```

### 12.3 Common Errors and Hints

| Error | Hint |
|-------|------|
| Unknown symbol `foo` | Did you mean: `filter`, `first`, `find`? |
| Wrong arity for `if` | `expected (if cond then else?)` |
| `let` bindings not paired | `let` requires an even number of binding forms |

---

## 13. Language Scope and Restrictions

PTC-Lisp intentionally omits many Clojure features for sandbox safety and simplicity, and supports a few (like anonymous functions) only with restrictions. For a complete list of intentional divergences with rationale, see [Clojure Conformance Gaps — Intentional Divergences](clojure-conformance-gaps.md#intentional-divergences-by-design-not-bugs).

Key omissions: lazy sequences, macros, mutable state (`atom`/`ref`/`agent`), `eval`/`read-string`, file I/O, `try`/`catch`/`throw`, multi-methods/protocols, user-defined namespaces, and full Java interop (minimal Date/Time subset supported: see §8.14).

**Note:** `println` IS supported — see §8.13. It writes to an internal trace buffer, not stdout.

### 13.1 Anonymous Functions (Supported, With Restrictions)

Anonymous functions are supported via `fn` or `#()` shorthand with restrictions:

#### Full `fn` Syntax

```clojure
(fn [x] body)           ; single argument
(fn [a b] body)         ; multiple arguments
(fn [a & rest] body)    ; variadic arguments
(fn [[a b]] body)       ; vector destructuring in params
(fn [{:keys [x]}] body) ; map destructuring in params (keyword keys)
(fn [{:strs [x]}] body) ; map destructuring in params (string keys)
```

**Implicit `do` (Clojure Extension):** Multiple body expressions are supported:

```clojure
(fn [x]
  (def last-input x)   ; side effect
  (* x 2))             ; return value
```

#### Short `#()` Syntax

The `#()` shorthand syntax provides concise lambdas (like Clojure):

```clojure
#(+ % 1)           ; % is the first parameter (p1)
#(+ %1 %2)         ; explicit numbered parameters
#(* % %)           ; same parameter used multiple times
#(42)              ; zero-arity thunk (no parameters)
#(vector %1 %&)    ; %& captures remaining args as a list
```

The `#()` syntax desugars to the equivalent `fn`:
- `#(+ % 1)` → `(fn [p1] (+ p1 1))`
- `#(+ %1 %2)` → `(fn [p1 p2] (+ p1 p2))`
- `#()` with no placeholders → `(fn [] ...)`
- `#(vector %1 %&)` → `(fn [p1 & rest] (vector p1 rest))`
- Arity is determined by the highest numbered placeholder, or 1 if only `%` is used
- `%&` captures remaining arguments as a list (like `& rest` in `fn`)

**Restrictions:**
- `#()` accepts a single expression as the body
- `%`, `%1`, `%2`, etc. are parameter placeholders; `%&` captures rest args (not regular symbols within `#()`)
- Nested `#()` is not allowed ([DIV-17](clojure-conformance-gaps.md#div-17-nested-not-allowed))
- Recursion is supported via `recur` (no self-reference by name, see §5.16)
- Closures over local `let` bindings are allowed
- No closures over mutable host state (there is none)

**Examples:**
```clojure
;; Filter with #() shorthand
(filter #(> % 10) items)

;; Map with string construction
(map #(str "id-" %) items)

;; Transform each item with fn (more complex)
(mapv (fn [u] (select-keys u [:name :email])) users)

;; Access outer let bindings (closure)
(let [threshold 100]
  (filter #(> (:price %) threshold) products))

;; Destructuring in fn params
(mapv (fn [{:keys [name age]}] {:name name :years age}) users)
```

**When to use `#()` vs `fn`:**
- Use a keyword (`:active`) as the predicate when you just need a truthy field check.
- Use `#()` for simple, single-argument lambdas (most common LLM use case).
- Use `fn` for complex logic, destructuring, or multiple parameters.

### 13.2 Functions Excluded from Core

For supported functions and special forms, see the [Function Reference](function-reference.md).

Key exclusions: `iterate`, `repeat`, `cycle` (infinite sequences), infinite `(range)` (finite `range` is supported: see §8.1), and transducers.

---

## 14. Grammar (EBNF)

```ebnf
program     = expression* ;  (* Multiple top-level expressions with implicit do *)

expression  = literal
            | symbol
            | keyword
            | vector
            | set
            | map
            | list-expr ;

literal     = nil | boolean | number | string | char ;

nil         = "nil" ;
boolean     = "true" | "false" ;
number      = integer | float ;
integer     = ["-"] digit+ ;
float       = ["-"] digit+ "." digit+ [exponent]
            | ["-"] digit+ exponent ;
exponent    = ("e" | "E") ["+" | "-"] digit+ ;
string      = '"' string-char* '"' ;
string-char = escape-seq | (any char except '"' and '\') ;  (* literal newlines allowed *)
escape-seq  = '\\' ('"' | '\\' | 'n' | 't' | 'r') ;
char        = '\\' (char-name | any-char) ;
char-name   = "newline" | "space" | "tab" | "return" | "backspace" | "formfeed" ;
any-char    = (any single Unicode grapheme) ;

symbol      = symbol-first symbol-rest* ;
symbol-first = letter | special-initial ;
symbol-rest  = letter | digit | special-rest ;
letter      = "a"-"z" | "A"-"Z" ;
digit       = "0"-"9" ;
special-initial = "+" | "-" | "*" | "/" | "<" | ">" | "=" | "?" | "!" | "_" | "%" | "." | "&" ;
special-rest    = special-initial | "'" ;  (* "'" only after the first char, e.g. inc' *)

keyword     = ":" keyword-char+ ;
keyword-char = letter | digit | "-" | "_" | "?" | "!" | "+" | "*" | "<" | ">" | "=" ;  (* no "/" in keywords *)

vector      = "[" expression* "]" ;

set         = "#{" expression* "}" ;

map         = "{" (map-entry)* "}" ;
map-entry   = expression expression ;

list-expr   = "(" expression expression* ")" ;  (* operator can be any expression *)

comment     = ";" (any char except newline)* newline ;

whitespace  = " " | "\t" | "\n" | "\r" | "," ;
```

**Grammar notes:**
- `/` is allowed in symbols for namespaced access (`data/bar`, `tool/bar`)
- `/` is NOT allowed in keywords (`:foo/bar` is invalid; see [DIV-13](clojure-conformance-gaps.md#div-13-namespaced-keywords-not-supported))
- The operator position in `list-expr` accepts any expression, enabling:
  - `(:name user)` — keyword as function
  - `((fn [x] x) 42)` — anonymous function application
  - `(tool/tool-name args)` — tool invocation

**Tokenization precedence:** When a token could match multiple grammar rules, literals take precedence over symbols:
1. `nil`, `true`, `false` → reserved literals (not symbols)
2. `-123`, `3.14` → numbers (not symbols starting with `-` or digits)
3. `:foo` → keyword
4. `\a`, `\newline` → character literal
5. Everything else → symbol

This means `-1` is always the integer negative one, never a symbol named "-1". Similarly, `\r` is the character "r", not a symbol.

---

## 15. Implementation Notes

### 15.1 Evaluation Model

- Programs can contain multiple expressions (evaluated sequentially, last value returned)
- Evaluation is strict (eager), not lazy
- No side effects except tool calls
- Tools may have side effects (external)

### 15.2 Resource Limits

| Resource | Default | Notes |
|----------|---------|-------|
| Timeout | 1,000 ms | Execution time limit |
| Max Heap | ~10 MB | Sandbox-process memory limit (1,250,000 words) |
| Worker Max Heap | = Max Heap | Fixed per-worker `pmap`/`pcalls` heap cap |
| Max Parallel Workers | 8 | Global cap on live `pmap`/`pcalls` workers |
| Max Tool Calls | unlimited (`nil`) | Per-program tool invocation limit; enforced only when set via the `:max_tool_calls` option |
| Loop/Recur Iterations | 1,000 | Per-loop/recur jump limit; ordinary non-tail recursion is bounded by timeout and heap |

Every `pmap`/`pcalls` worker process — top-level *and* nested — is
spawned with a **fixed** `max_heap_size` of `Worker Max Heap` words (the
cap is NOT divided by concurrency). The number of such workers alive at
once, across the whole program and at every nesting depth, is bounded by
a shared slot budget of `Max Parallel Workers`. Aggregate live parallel
heap is therefore bounded by `Max Parallel Workers × Worker Max Heap`.

- A worker that exceeds its fixed heap cap is killed; the program fails
  with `:memory_exceeded`.
- A `pmap`/`pcalls` that cannot obtain a worker slot — e.g. deeply
  nested parallelism that would exceed the global budget — fails with
  `:parallel_capacity_exceeded` (there is no sequential fallback).
- The whole parallel operation (including nested `pmap`/`pcalls`) shares
  one deadline derived from `pmap_timeout`; exceeding it fails with
  `:timeout`.

*Note: Hosts may configure higher timeouts (e.g., 5,000ms) to accommodate slow tool calls.*

### 15.3 Compatibility Testing

Programs should produce identical results when run in:
1. PTC-Lisp interpreter (Elixir)
2. Clojure (with stub implementations for `data/`, `tool/`, `call`, and other PTC-specific forms)

---

## 16. Memory Model for Agentic Loops

This section specifies how PTC-Lisp programs interact with persistent memory across multiple turns in an LLM-agent loop.

### 16.1 Core Principle: Functional Transactions

Programs are **pure functions** that:
- Read from stored values (plain symbols) and `data/` namespace
- Return a result value
- The result determines stored value updates

This provides **transactional semantics**: either the entire program succeeds and memory updates, or it fails and memory remains unchanged.

### 16.2 Environment Structure

The host builds an execution environment for each program:

```elixir
%{
  memory: %{                    # Persistent across turns
    high_paid: [...],
    query_count: 5,
    ...
  },
  ctx: %{                       # Current request only
    input: [...],
    user_id: "user-123",
    request_id: "req-456",
    ...
  },
  tools: %{                     # Registered tool functions
    "get-users" => &Host.get_users/1,
    "get-orders" => &Host.get_orders/1,
    ...
  },
  __meta__: %{                  # Execution metadata (not exposed to DSL)
    call_id: "uuid-...",
    turn: 3,
    retry_count: 0,
    timestamp: ~U[2024-01-15 10:30:00Z],
    limits: %{max_tool_calls: nil, timeout_ms: 1000}   # illustrative; max_tool_calls defaults to nil (unlimited), sandbox timeout to 1000 ms
  }
}
```

### 16.3 Result Contract (V2 Simplified Model)

The program's return value is passed through unchanged. Storage is explicit via `def`:

| Behavior | How It Works |
|----------|--------------|
| **Return value** | Last expression result (standard REPL semantics) |
| **Persistent storage** | Use `(def name value)` to store values |
| **Access stored values** | Use plain symbols (e.g., `my-value`) |

**No implicit map merge.** Unlike earlier versions, returning a map does NOT automatically store its keys. Use `def` for explicit storage.

#### Pure Query (No Storage)

```clojure
;; Returns a number - nothing stored
(->> data/expenses
     (filter (fn [e] (= (:category e) "travel")))
     (sum-by :amount))
```

#### Explicit Storage with def

```clojure
;; Store values explicitly, return a result
(def high-paid (->> (tool/find-employees {})
                    (filter (fn [e] (> (:salary e) 100000)))))
(def last-query "employees")
(map :email high-paid)
```

After execution:
- `high-paid` = the filtered list (available as symbol in next turn)
- `last-query` = `"employees"` (available as symbol in next turn)
- Return value = `["alice@example.com", "bob@example.com", ...]`

#### Return Map Without Storage

Maps return as-is, no special handling:

```clojure
;; Returns a map - nothing stored unless you use def
{:summary "Query complete"
 :count (count data/items)
 :items data/items}
```

Return value = `{:summary "Query complete", :count 5, :items [...]}`, no symbols stored.

### 16.4 Symbol Storage Semantics

Values stored via `def` persist across turns. Each `def` sets a single key:

```clojure
;; Turn 1: Store values
(def a 1)
(def b {:x 10})
"stored"

;; Turn 2: Access and update
(def b {:y 20})  ; replaces previous value
(def c 3)        ; new value
{:a a, :b b, :c c}
```

After Turn 2: `a=1, b={:y 20}, c=3`

- New symbols are added
- Existing symbols are replaced (not deep-merged)
- Symbols not referenced remain unchanged

### 16.5 Execution Flow

```
┌─────────────────────────────────────────────────────────────────┐
│  AGENTIC LOOP EXECUTION FLOW                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. HOST BUILDS ENVIRONMENT                                     │
│     ├─ Load stored symbols from previous turns (def bindings)   │
│     ├─ Attach current request context                           │
│     └─ Register available tools                                 │
│                                                                 │
│  2. RECEIVE PROGRAM FROM LLM                                    │
│     └─ Parse source → AST                                       │
│                                                                 │
│  3. EXECUTE IN SANDBOX                                          │
│     ├─ Validate AST                                             │
│     ├─ Evaluate with resource limits                            │
│     ├─ Track def bindings (become symbols for next turn)        │
│     └─ Track tool calls for logging                             │
│                                                                 │
│  4. HANDLE RESULT                                               │
│     │                                                           │
│     ├─ ON SUCCESS:                                              │
│     │   ├─ Return last expression value (standard REPL)         │
│     │   ├─ Persist def bindings as symbols                      │
│     │   └─ Log: program, tool calls, stored symbols, result     │
│     │                                                           │
│     └─ ON ERROR:                                                │
│         ├─ NO symbol changes (rollback)                         │
│         ├─ Log: program, error, partial trace                   │
│         └─ Return error to LLM for retry                        │
│                                                                 │
│  5. NEXT TURN                                                   │
│     ├─ Feed stored symbols to LLM                               │
│     └─ LLM generates next program                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### 16.6 Multi-Turn Example

**Turn 1:** Find high-paid employees and store with def

```clojure
(def high-paid (->> (tool/find-employees {})
                    (filter (fn [e] (> (:salary e) 100000)))))
(count high-paid)
```

*Returns:* `5`
*Symbols stored:* `{:high-paid [{:id 1, :name "Alice", :salary 150000}, ...]}`

**Turn 2:** Query stored data (no symbol update)

```clojure
(count high-paid)
```

*Returns:* `5`
*Symbols unchanged*

**Turn 3:** Fetch orders for stored employees, add new symbol

```clojure
(def orders (let [ids (map :id high-paid)]
              (tool/get-orders {:employee-ids ids})))
{:orders-count (count orders)}
```

*Returns:* `{:orders-count 42}`
*Symbols stored:* `{:high-paid [...], :orders [...]}`

**Turn 4:** Return summary

```clojure
{:employee-count (count high-paid)
 :order-count (count orders)}
```

*Returns:* `{:employee-count 5, :order-count 42}`
*Symbols unchanged*

### 16.7 Logging and Audit Trail

Every execution produces a log entry:

```elixir
%{
  call_id: "uuid-...",
  turn: 3,
  timestamp: ~U[2024-01-15 10:30:00Z],

  # Input
  program_source: "(do (def orders (call \"get-orders\" {:ids (map :id high-paid)})) ...)",
  memory_before: %{high_paid: [...]},
  ctx: %{user_id: "user-123"},

  # Execution trace
  tool_calls: [
    %{tool: "get-orders", args: %{ids: [1, 2, 3]},
      result_size: 42, duration_ms: 150}
  ],

  # Output
  status: :success,  # or :error
  result: {:orders-count 42},  # last expression value
  memory_after: %{"high_paid" => [...], "orders" => [...]},  # includes def bindings

  # Metrics
  duration_ms: 180,
  memory_bytes: 102400
}
```

### 16.8 Resource Limits for Agentic Execution

| Limit | Default | Description |
|-------|---------|-------------|
| `timeout_ms` | 1,000 | Max execution time per program |
| `max_heap` | ~10 MB | Memory limit (1,250,000 words) |
| `max_tool_calls` | unlimited (`nil`) | Max tool invocations per program; no limit unless explicitly set |

*Note: Hosts can configure higher timeouts (e.g., 5,000ms) to accommodate slow tool calls.*

On limit violation:
- Execution aborts immediately
- No memory changes (transaction rollback)
- Error returned to LLM with limit details
- LLM can retry with a modified program

### 16.9 Error Handling in Agentic Loops

Errors are designed to be **LLM-recoverable**:

```elixir
# Error structure
{:error, %{
  type: :tool_call_limit_exceeded,
  message: "Program made 12 tool calls, limit is 10",
  context: %{
    limit: 10,
    actual: 12,
    last_tool: "get-orders"
  },
  hint: "Consider batching requests or filtering data before tool calls"
}}
```

The LLM receives this error and can generate a corrected program.

### 16.10 Security Considerations

| Concern | Mitigation |
|---------|------------|
| Memory exhaustion | Max memory size limit |
| Infinite loops | Timeout + loop iteration limit (default 1000) |
| Unbounded recursion | Timeout + memory limit |
| Tool abuse | Per-program tool call limit |
| Data exfiltration | Tools are host-controlled, audited |
| Memory pollution | Explicit `def` storage only |
| Cross-turn attacks | Memory is agent-scoped, not shared |

---

## Appendix A: Symbol Resolution

### Resolution Order

When the interpreter encounters a plain symbol, it resolves in this order:

1. **Local bindings** — `let`-/`loop`-bound variables in current scope
2. **`def` bindings** — values stored via `def`/`defn` (the User Namespace; persists across turns and shadows builtins)
3. **Built-in functions** — `filter`, `map`, `count`, etc.

Namespaced accesses (`data/y`, `tool/z`, `budget/remaining`) are *not* part of this plain-symbol chain — they are dispatched by a separate AST path before plain-symbol lookup is reached.

### Namespace Symbols

| Pattern | Resolves To |
|---------|-------------|
| `data/bar` | `(get env.data :bar)` |
| `tool/baz` | Tool invocation |
| `budget/remaining` | Remaining tool call budget |
| `tool/servers`, `apropos`, `dir`, `doc`, `meta`, `ns-publics` | REPL discovery |
| `foo` | Local binding, `def` binding, or built-in |

### Example

```clojure
(let [x 10]                    ; x is local
  (+ x                         ; resolves to local x (10)
     data/x))                  ; resolves to env.data[:x]
```

### Whole Map Access

The bare symbol `data` is **not accessible** as a whole map. Only namespaced access is allowed:

```clojure
data/bar       ; OK - access :bar key
data           ; ERROR - cannot access whole data map
(keys data)    ; ERROR - data is not a value
```

This restriction prevents accidental data leakage and simplifies reasoning about what data a program can access.

---

## Appendix B: Documentation Tests

This specification contains executable examples that are automatically validated against the PTC-Lisp implementation using `PtcRunner.Lisp.SpecValidator`.

### Example Syntax

Examples use the pattern `code  ; => expected` where the expected value is parsed and compared to the actual execution result:

```clojure
(+ 1 2)                ; => 3
(filter even? [1 2 3]) ; => [2]
{:a 1 :b 2}            ; => {:a 1 :b 2}
```

### Semantic Markers

For examples that cannot be automatically validated, use these markers:

| Marker | Meaning | Example |
|--------|---------|---------|
| `; => TODO: description` | Feature not yet implemented | `; => TODO: :or defaults not implemented` |
| `; => BUG: description` | Known bug | `; => BUG: edge case fails` |
| `; => ...` | Illustrative example (requires external context) | `; => ...` |

**When to use each:**

- **TODO** — The feature is documented but the implementation is incomplete. Running the example would fail.
- **BUG** — The example documents expected behavior but currently fails due to a known bug.
- **...** — The example requires external context (tools, data/memory data) that isn't available during automated testing. These are illustrative examples showing usage patterns.

### Running Validation

```elixir
# Validate all examples
{:ok, results} = PtcRunner.Lisp.SpecValidator.validate_spec()

# Results include:
# - passed: count of passing examples
# - failed: count of failing examples
# - todos: list of {code, description, section} tuples
# - bugs: list of {code, description, section} tuples
# - skipped: count of illustrative examples (using ...)
```


### Supported Expected Values

The validator can parse these value types:

- **Literals**: `nil`, `true`, `false`, integers (`42`), floats (`3.14`)
- **Strings**: `"hello"` (with escape sequences)
- **Keywords**: `:name`, `:user-id`
- **Collections**: `[1 2 3]`, `(1 2 3)`
- **Maps**: `{:a 1 :b 2}` (simple keyword/value pairs only)
