# Phel: Full Documentation Bundle
> Plain-markdown bundle of the core Phel documentation for LLM ingestion.
> Generated by scripts/generate-llms-full.js. Edit the source pages, not this file.
> Index and links: https://phel-lang.org/llms.txt
---
# Agentic Coding
> Source: https://phel-lang.org/documentation/reference/agentic-coding/
Single-page reference for AI agents (Claude Code, Codex, Cursor, Copilot, Aider, Gemini) to learn Phel without crawling the docs. Humans pairing with an agent benefit too.
Load this one if you can only load one doc into an agent's context.
> **Want this installed as a skill in your tool?** [Agent Setup](/documentation/reference/agent-setup/) wires Claude Code, Cursor, Copilot, Codex, Gemini, or Aider to Phel with one `phel agent-install` command.
⤓ Download raw markdown
For agents and scripts: curl https://phel-lang.org/agentic-coding.md. Same body, no HTML chrome.
## TL;DR for agents
Truncation-safe rules. Code form first, reason second. Verify with `phel doc` before deviating.
| Use | Avoid | Why |
|----------------------------------------------------------------------------------|---------------------------------------------------------------|----------------------------------------------------------------|
| `phel doc `, grep `vendor/phel-lang/phel-lang/src/phel/core/` | inventing names | Hallucinated symbols compile then fail at runtime. |
| `phel.string` (alias `str`) | `phel.str`, `clojure.string`, `php/strtoupper`, `php/explode` | `phel.str` removed. Phel string fns return Phel values. |
| `(ns app.main)` (≥2 segments, file mirrors path under `src/`) | `(ns main)` | Single-segment ns exports invalid PHP under `phel build`. |
| `argv` (vector of strings) | `*argv*` (pre-0.39), `php/$argv` | Symbol renamed in 0.39. `php/$argv` is `nil` under `phel run`. |
| `for` for data, `foreach`/`doseq` for effects | `for` with side effects | `for` returns a vector. `foreach` returns `nil`. |
| `recur` in tail of `loop`/`fn` | `recur` anywhere else | Non-tail `recur` errors at compile time. |
| `vec` (PHP→Phel), `to-php-array` (Phel→PHP) | treating PHP arrays as Phel collections | Different types. Mixing breaks `count`, `map`, etc. |
| `#php {"k" "v"}` for PHP assoc | `{:k "v"}` as a PHP array | Phel maps are not PHP arrays. |
| `(:x p)` or `(get p :x)` for records | `(.-x p)` | Record fields are protected PHP properties. |
| `false`, `nil` only as falsy | assuming `0`, `""`, `[]`, `{}` falsy | All four are truthy. |
| `(when-not *build-mode* ...)` around top-level effects | unguarded top-level effects | `phel build` evaluates top level; effects fire at build time. |
| Verify Clojure-looking forms first ([Phel is not Clojure](#phel-is-not-clojure)) | porting Clojure code blindly | PHP target, not JVM. Different stdlib, different concurrency. |
## What Phel is
Functional Lisp that compiles to PHP. Runs on any PHP 8.4+, ships via Composer, full PHP interop.
- Immutable persistent data structures.
- Macros, homoiconicity, REPL-driven dev.
- Compiles to plain PHP. No separate runtime, no JVM.
- Source: `.phel`. Config: `phel-config.php`.
## CLI cheat sheet
```bash
vendor/bin/phel doc # function signature + docstring
vendor/bin/phel eval '' # one-shot eval
vendor/bin/phel repl # full REPL
vendor/bin/phel test [path] # run tests
vendor/bin/phel run # run a script
vendor/bin/phel build # compile to PHP
vendor/bin/phel format # rewrite formatting
vendor/bin/phel doctor # env + extension check
```
### Reliable multi-line evaluation
Pass a quoted heredoc to `eval -`:
```bash
vendor/bin/phel eval - <<'PHEL'
(ns app)
(println (+ 40 2))
PHEL
```
Prefer this pattern because:
- **No quoting issues:** Everything between `<<'PHEL'` and `PHEL` is treated as literal input.
- **Consistent pattern:** One approach works for all evaluations, from simple to complex.
- **Multi-line friendly:** Code keeps its natural, readable formatting.
- **Easy to extend:** Add more forms without changing the command syntax.
## Installed agent skills
Phel ships skill adapters in `vendor/phel-lang/phel-lang/.agents/`. Install for the active agent:
```bash
vendor/bin/phel agent-install claude # or codex, cursor, copilot, aider, gemini
vendor/bin/phel agent-install --all # every adapter
```
`.agents/` contains: `RULES.md`, `index.md` (intent map), `tasks/*.md` (HTTP apps, CLI tools, tests, REPL flow, validation), `examples/`. Prefer it over guessing.
## Syntax in 60 seconds
```phel
;; Inline comment uses one semicolon.
;; Standalone comment uses two.
;; Atoms: nil true false
;; Numbers: 42 -3 1.5 3.14e2 0xFF 0b1010 0o17
;; Strings: "hello" "line\nbreak"
;; Keywords: :status :user/email
;; Symbols: my-var my-ns/fn
;; Regex literal: #"^\d+$"
;; Calls: (function arg1 arg2 ...). First element is the operator.
(+ 1 2 3) ; => 6
(str "Hello, " name)
;; Data structures (all immutable):
[1 2 3] ; vector
{:a 1 :b 2} ; map
#{1 2 3} ; set
'(1 2 3) ; list (data, not a call)
;; PHP assoc array literal (when interop needs one):
#php {"k" "v"}
```
## Core Forms
```phel
(def x 42) ; global binding
(def- secret 7) ; private binding
(defn greet [name] ; public function
(str "Hello, " name))
(defn- helper [x] (* x 2)) ; private function
(let [x 1, y 2] (+ x y)) ; local bindings (commas optional)
(if cond then else)
(when cond expr ...)
(cond pred-1 expr-1
pred-2 expr-2
:else fallback)
(case x 1 "one" 2 "two" "default")
(condp = x 1 "one" 2 "two" "other")
(do expr1 expr2 ... last) ; sequence; returns last
(loop [acc 0 n 10]
(if (zero? n) acc (recur (+ acc n) (dec n))))
(for [x :in xs :when (odd? x)] (* x x)) ; comprehension, returns vector
(foreach [x xs] (println x)) ; side effects, returns nil
(dotimes [i 5] (println i))
(fn [x] (* x 2)) ; anonymous fn
#(* % 2) ; reader shorthand (single arg)
#(+ %1 %2) ; multi-arg shorthand
#(apply + %&) ; variadic shorthand
(-> x (f a) (g b)) ; thread first
(->> x (f a) (g b)) ; thread last
(some-> x .a .b) ; nil-safe thread first
(cond-> x pred (f y)) ; conditional thread
(try expr (catch Exception e (handle e)) (finally cleanup))
```
## Namespaces
File `src/my-app/users.phel`:
```phel
(ns my-app.users
(:require phel.string :as str)
(:require phel.html :as h)
(:use DateTimeImmutable))
(defn full-name [{:first f :last l}]
(str/join " " [f l]))
```
Rules:
- Two or more segments required (`my-app.main`, not `main`).
- File path mirrors namespace under `src/`. Source uses dashes, compiled PHP uses studly case (`my-app.users` ↔ `MyApp\Users`).
## PHP Interop
```phel
(php/strlen "hi") ; call PHP function
(php/new DateTimeImmutable "2024-01-15") ; construct
(php/-> obj (method arg)) ; instance method
(php/:: DateTimeImmutable ATOM) ; static / constant
;; Shorthands also accepted:
(.method obj arg)
(.-prop obj)
(Class/method args)
Class/CONST
;; Convert Phel collection to PHP array (when handing off to PHP):
(to-php-array ["a" "b" "c"])
;; Convert PHP array back to Phel collection:
(vec (php/explode "," "a,b,c")) ; => ["a" "b" "c"]
;; Or with phel.string (returns Phel vector directly):
;; (phel.string/split "a,b,c" #",")
;; Catch PHP exceptions:
(try (risky)
(catch RuntimeException e (handle e)))
```
## Records, Protocols, Multimethods
```phel
(defrecord Point [x y])
(def p (->Point 1 2))
(:x p) ; => 1 (keyword-as-fn: preferred)
(get p :x) ; => 1 (also valid)
(map->Point {:x 1 :y 2}) ; => (point 1 2)
(defprotocol Drawable
(draw [this]))
(extend-type :string Drawable
(draw [s] (println s)))
(defmulti area :shape)
(defmethod area :circle [{:radius r}] (* 3.14 r r))
(defmethod area :rect [{:w w :h h}] (* w h))
```
## Equality and comments
- `=` is value equality across all types. `identical?` is reference equality.
- Comments: `;` inline, `;;` standalone, `#_` discards the next form, `(comment ...)` ignores its body.
```phel
(= [1 2] [1 2]) ; => true
#_(this-form-is-skipped)
```
Truthiness is in the [TL;DR](#tl-dr-for-agents).
## Tests
```phel
(ns my-app.users-test
(:require phel.test :refer [deftest is])
(:require my-app.users :as users))
(deftest full-name-joins
(is (= "Ada Lovelace"
(users/full-name {:first "Ada" :last "Lovelace"}))))
```
Run with `vendor/bin/phel test`.
## Other gotchas
Beyond the TL;DR:
- **`transduce` with `max`/`min`:** no zero-arity. Pass init: `(transduce xf (fn [a b] (max a b)) 0 coll)`.
- **No `to-vec` / `to-list` functions.** Use `vec` (PHP array to Phel vector) or `to-php-array` (Phel to PHP).
- **`recur` arity must match `loop` bindings.** Mismatched arg count errors at compile time.
- **`#` line comments are deprecated.** Use `;` or `;;`.
## Phel is not Clojure
Agents trained on Clojure data hallucinate Clojure-only forms in Phel code. Phel is Lisp-on-PHP, not Lisp-on-JVM. Verify with `phel doc ` before using anything that "sounds Clojure".
Known differences:
- **Strings module:** `phel.string`, not `clojure.string`. Some function names match, some don't. Check each.
- **Interop is PHP, not Java.** `(php/new Class arg)`, `(.method obj)`, `(Class/method)`, `Class/CONST`. No `Class/.method`, no `Class.`, no JVM.
- **Records:** field access by keyword `(:x p)`. No `.-field` on records.
- **Numbers:** PHP `int`/`float`, plus Phel `:ratio` (`(/ 1 3)` => `1/3`) and `:bigint` (auto-promoted on overflow). No `BigDecimal`.
- **Reader conditionals use `:phel`/`:default`,** not `:clj`/`:cljs`. Example: `#?(:phel "phel" :default "other")`.
- **Concurrency primitives are fiber-based.** `atom`, `future`, `promise`, `pmap`, `async`/`await`, `await-all`, `await-any` all exist (see `phel/core/async.phel`). `ref`, `agent`, STM do not. Verify each with `phel doc`.
- **No `clojure.*` namespaces.** `clojure.set`, `clojure.walk`, `clojure.spec`, `clojure.test.check`, `core.match`: none. Phel modules live under `phel.*` (`phel.string`, `phel.html`, `phel.test`, etc).
- **`phel.test`, not `clojure.test`.** Uses `deftest` + `is`.
- **Type tags emit PHP declarations**, not Java. `^int`, `^string`, `^"?int"` on `defn` params/return.
When in doubt: run `phel doc `. If it errors, it does not exist; do not generate code that calls it.
## Project layout
```
my-app/
composer.json # PHP deps + composer scripts (repl, dev, test, build)
phel-config.php # Phel config; usually one line via forProject()
src/
main.phel # entry namespace
modules/...
tests/
modules/...
```
Minimal `phel-config.php`:
```php
> xs (filter f) (map g) (reduce h 0))` beats deep nesting.
3. **Stay immutable.** `(conj v x)` returns a new vector. Rebind, don't expect mutation.
4. **Interop shorthands.** `(.method obj)`, `(.-prop obj)`, `(Class/method)`, `(ClassName.)`. Shorter, idiomatic.
5. **`^:memoize` for caching.** `(defn ^:memoize f [x] ...)` beats a manual `static $cache` pattern.
6. **Type tags emit PHP declarations.** `^int`, `^string`, `^"?int"` on `defn` params/return = free PHP type hints.
7. **No em-dashes** in docstrings or generated site docs. Use commas, colons, periods, parentheses.
8. **Conventional commits.** `feat:`, `fix:`, `ref:`, `chore:`, `docs:`, `test:`. No AI/LLM authorship references.
## Where to look next
In the Phel install:
- `vendor/phel-lang/phel-lang/.agents/index.md`: intent → recipe map.
- `vendor/phel-lang/phel-lang/.agents/RULES.md`: canonical rules + CLI map.
- `vendor/phel-lang/phel-lang/.agents/tasks/`: HTTP, CLI, tests, debugging, validation, pattern matching.
- `vendor/phel-lang/phel-lang/src/phel/core/`: every core function source.
On this site:
- [Cheat Sheet](/documentation/reference/cheat-sheet): core forms and functions.
- [Language section](/documentation/language/): types, functions, control flow, macros, interfaces, namespaces, destructuring, recursion.
- [PHP Interop](/documentation/php-interop): every interop form.
- [Cookbook](/documentation/guides/cookbook): copy-paste recipes.
- [Rosetta Stone](/documentation/guides/rosetta-stone): PHP to Phel side-by-side.
- [REPL guide](/documentation/tooling/repl): dev loop.
- [CLI Commands](/documentation/tooling/cli-commands): every subcommand.
---
# Cheat Sheet
> Source: https://phel-lang.org/documentation/reference/cheat-sheet/
Quick reference for Phel syntax and core functions.
> **AI agents:** load [Agentic Coding](/documentation/reference/agentic-coding) first for the truncation-safe rules and PHP-interop gotchas. This sheet is the wide surface; that one is the must-know.
## Basic syntax
```phel
;; This is a standalone comment
; inline comment (after an expression)
nil ; null value
true false ; booleans (only false and nil are falsy)
42 -3 1.5 3.14e2 ; numbers
0xFF 0b1010 017 ; hex, binary, octal
"hello" "line\nbreak" ; strings
:keyword :status ; keywords (interned constants)
my-var my-module/fn ; symbols
#"[a-z]+" ; regex literal (PCRE pattern)
```
> **Note:** `#` line and `#| |#` multiline comments are deprecated. Use `;;` for standalone comments and `;` for inline comments.
See [Basic Types](/documentation/language/basic-types).
## Reader syntax
```phel
@my-var ; shorthand for (deref my-var)
#"pattern" ; regex literal (PCRE)
#(+ %1 %2) ; anonymous function shorthand
#(inc %) ; single-arg: % is the same as %1
#(apply + %&) ; variadic: %& captures rest args
#?(:phel expr1 :default expr2) ; reader conditional
#?@(:phel [a b] :default [c]) ; splicing reader conditional
;; Tagged literals
#inst "2026-01-15T12:00:00Z" ; => DateTimeImmutable
#uuid "550e8400-e29b-41d4-a716-446655440000" ; => UUID string
#regex "\\d+" ; => PCRE pattern string
;; First-class var handles
#'my-fn ; shorthand for (var my-fn)
(var my-fn) ; returns the Var object for my-fn
```
`#(...)` is the preferred shorthand. `%` or `%1` first arg, `%2` second, `%&` rest. Legacy `|(...)` with `$` is deprecated.
Reader conditionals (`#?()`, `#?@()`) target platforms in `.cljc` via `:phel` and `:default`.
Tagged literals: `#inst` reads as `DateTimeImmutable`, `#uuid` as a UUID string, `#regex` as a PCRE pattern. Register custom tags with `register-tag` from `phel.reader`.
## Data structures
```phel
[1 2 3] ; vector (indexed)
(vector 1 2 3) ; same thing
{:a 1 :b 2} ; map (key-value pairs)
(hash-map :a 1 :b 2) ; same thing
#{1 2 3} ; set (unique values)
(hash-set 1 2 3) ; set from arguments
(set [1 2 3]) ; coerce collection to set
'(1 2 3) ; quoted list (data, not a call)
(list 1 2 3) ; same thing
```
See [Data Structures](/documentation/language/data-structures).
## Accessing data
```phel
(get [1 2 3] 0) ; => 1
(get {:a 1} :a) ; => 1
(get {:a 1} :b "default") ; => "default"
(get-in {:a {:b 1}} [:a :b]) ; => 1
(first [1 2 3]) ; => 1
(second [1 2 3]) ; => 2
(peek [1 2 3]) ; => 3
(:name {:name "Alice"}) ; => "Alice" (keyword as function)
({:a 1 :b 2} :a) ; => 1 (map as function)
([10 20 30] 1) ; => 20 (vector as function)
```
## Modifying data
```phel
(conj [1 2] 3) ; => [1 2 3]
(conj #{1 2} 3) ; => #{1 2 3}
(conj {:a 1} [:b 2]) ; => {:a 1, :b 2}
(assoc {:a 1} :b 2) ; => {:a 1, :b 2}
(assoc [1 2 3] 0 9) ; => [9 2 3]
(dissoc {:a 1 :b 2} :a) ; => {:b 2}
(update {:a 1} :a inc) ; => {:a 2}
(update-keys {:a 1 :b 2} name) ; => {"a" 1, "b" 2}
(update-vals {:a 1 :b 2} inc) ; => {:a 2, :b 3}
(assoc-in {} [:a :b] 1) ; => {:a {:b 1}}
(update-in {:a {:b 1}} [:a :b] inc) ; => {:a {:b 2}}
(merge {:a 1} {:b 2 :a 3}) ; => {:a 3, :b 2}
```
See [Data Structures](/documentation/language/data-structures).
## Destructuring
```phel
;; Sequential destructuring
(let [[a b c] [1 2 3]]
(+ a b c)) ; => 6
(let [[a b & rest] [1 2 3 4 5]]
rest) ; => [3 4 5]
;; Associative destructuring
(let [{:name name :age age} {:name "Alice" :age 30}]
(str name " is " age)) ; => "Alice is 30"
;; Default values
(let [{:name name :role role :or {role "guest"}}
{:name "Bob"}]
role) ; => "guest"
;; Works in defn, fn, loop too
(defn greet [{:name name}]
(str "Hello, " name))
(greet {:name "Alice"}) ; => "Hello, Alice"
```
See [Destructuring](/documentation/language/destructuring).
## Defining things
```phel
(def pi 3.14159) ; global binding
(def secret :private 42) ; private binding
(defonce conn (connect!)) ; bind once; skipped if already defined (survives REPL reloads)
(defn greet [name] ; public function
(str "Hello, " name))
(defn- helper [x] ; private function
(* x 2))
(defstruct point [x y]) ; struct (typed map)
(point 1 2) ; => (point 1 2)
(point? (point 1 2)) ; => true
(let [x 1 ; local bindings
y (+ x 2)]
(+ x y)) ; => 4
(defmulti area :shape) ; multimethod (dispatch on :shape)
(defmethod area :circle [{:radius r}]
(* 3.14 r r))
```
See [Global and Local Bindings](/documentation/language/global-and-local-bindings).
## Functions
```phel
(fn [x] (* x 2)) ; anonymous function
#(* % 2) ; short form (single param)
#(+ %1 %2) ; short form (multiple params)
#(apply + %&) ; short form (variadic)
(defn greet ; multi-arity
([] "Hi")
([name] (str "Hi " name)))
(defn sum [& nums] ; variadic
(apply + nums))
(apply + [1 2 3]) ; => 6
(partial + 10) ; => fn that adds 10
(comp inc inc) ; => fn that increments twice
(identity 42) ; => 42
(some-fn pos? even?) ; => fn: true if any predicate passes
(every-pred pos? even?) ; => fn: true only if every predicate passes
(memoize expensive-fn) ; => cached version of fn
(memoize-lru expensive-fn 100) ; => cached with max 100 entries
(defn ^:memoize fib [n] ...) ; defn metadata: auto-wraps in memoize
(defn ^{:memoize-lru 128} f [k] ...)
(defn ^:async fetch [url] ...) ; wraps body in (async ...) -> Amp\Future
(defn ^int add [^int a ^int b] ; :tag metadata -> PHP type decls
(+ a b))
```
See [Functions and Recursion](/documentation/language/functions-and-recursion).
## Control flow
```phel
(if (> x 0) "pos" "non-pos") ; if/else
(when (> x 0) (print "pos")) ; when (no else branch)
(cond
(< n 0) "negative"
(= n 0) "zero"
:else "positive")
(case status
200 "OK"
404 "Not Found")
(do (print "a") (print "b") 42) ; evaluate multiple exprs, return last
```
See [Control Flow](/documentation/language/control-flow).
## Loops & recursion
```phel
(loop [acc 0 n 10] ; loop with recur
(if (= n 0)
acc
(recur (+ acc n) (dec n)))) ; => 55
(foreach [v [1 2 3]] ; side-effects only, returns nil
(print v))
(for [x :in [1 2 3]] (* x 2)) ; => [2 4 6] (list comprehension)
(for [x :range [0 5]] x) ; => [0 1 2 3 4]
(for [x :in [1 2 3 4]
:when (even? x)] x) ; => [2 4]
(dotimes [i 3] (print i)) ; prints 0, 1, 2
(def n (atom 0))
(while (< @n 3) (swap! n inc)) ; side-effects while test is truthy
@n ; => 3
```
See [Functions and Recursion](/documentation/language/functions-and-recursion), [Control Flow](/documentation/language/control-flow).
## Collections
```phel
(def users [{:role :admin} {:role :user} {:role :admin}])
(map inc [1 2 3]) ; => @[2 3 4]
(filter even? [1 2 3 4]) ; => @[2 4]
(mapv inc [1 2 3]) ; => [2 3 4] (eager, returns a vector)
(filterv even? [1 2 3 4]) ; => [2 4] (eager, returns a vector)
(reduce + 0 [1 2 3]) ; => 6
(sort [3 1 2]) ; => [1 2 3]
(sort-by :age [{:age 30} {:age 20}]) ; sort by key
(group-by :role users) ; map of role -> [users]
(frequencies [:a :b :a :a]) ; => {:a 3, :b 1}
(count [1 2 3]) ; => 3
(empty? []) ; => true
(contains? {:a 1} :a) ; => true
(some even? [1 3 4]) ; => true
(every? pos? [1 2 3]) ; => true
(bounded-count 3 [1 2 3 4 5]) ; => 5 (walks at most 3 of a non-counted? seq)
(into #{} [1 2 1 3]) ; => #{1 2 3}
(vec '(1 2 3)) ; => [1 2 3] (coerce to vector)
(subset? #{1 2} #{1 2 3}) ; => true
(superset? #{1 2 3} #{1 2}) ; => true
(distinct [1 2 1 3 2]) ; => @[1 2 3]
(distinct? 1 2 3) ; => true (no two arguments are =)
(splitv-at 2 [1 2 3 4 5]) ; => [[1 2] [3 4 5]] (eager split)
(map-invert {:a 1 :b 2}) ; => {1 :a, 2 :b} (swap keys and values)
(flatten [[1 2] [3 [4]]]) ; => @[1 2 3 4]
(reverse [1 2 3]) ; => [3 2 1]
(concat [1 2] [3 4]) ; => @[1 2 3 4]
(compact [1 nil 2 nil 3]) ; => @[1 2 3]
(remove neg? [1 -2 3 -4]) ; => @[1 3]
```
See [Data Structures](/documentation/language/data-structures).
## Sorted collections & set relations
```phel
(def sm (sorted-map 1 :a 3 :b 5 :c))
(subseq sm >= 3) ; => @[[3 :b] [5 :c]] (ascending range query)
(rsubseq sm <= 3) ; => @[[3 :b] [1 :a]] (descending)
;; Relational helpers over sets of maps, in the spirit of clojure.set
(def rel #{{:id 1 :role :admin} {:id 2 :role :user}})
(select #(= (:role %) :admin) rel) ; => #
(project rel [:role]) ; => #{{:role :admin} {:role :user}}
(rename rel {:role :kind}) ; => rows with :role renamed to :kind
(index rel [:role]) ; => map of {:role X} -> set of matching rows
```
`subseq` and `rsubseq` are lazy and honor the collection's comparator, so they only walk the matching range.
## Walking data structures
Requires `(:require phel.walk :refer [postwalk prewalk postwalk-replace keywordize-keys stringify-keys])`.
```phel
(postwalk f nested) ; transform bottom-up
(prewalk f nested) ; transform top-down
(postwalk-replace {:a :x} [:a :b]) ; => [:x :b]
(keywordize-keys {"name" "Alice"}) ; => {:name "Alice"}
(stringify-keys {:name "Alice"}) ; => {"name" "Alice"}
```
See [Data Structures](/documentation/language/data-structures/#walking-data-structures).
## Lazy sequences
```phel
(take 5 (range)) ; => @[0 1 2 3 4]
(take 5 (iterate inc 0)) ; => @[0 1 2 3 4]
(take 7 (cycle [1 2 3])) ; => @[1 2 3 1 2 3 1]
(take 4 (repeat :x)) ; => @[:x :x :x :x]
(take 5 (repeatedly #(php/rand 1 100))) ; 5 random numbers
(drop 3 (range 10)) ; => @[3 4 5 6 7 8 9]
(take-while pos? [3 2 1 0 -1]) ; => @[3 2 1]
(drop-while pos? [3 2 1 0 -1]) ; => @[0 -1]
(partition 2 [1 2 3 4 5 6]) ; => @[[1 2] [3 4] [5 6]]
(partition 2 1 [1 2 3 4]) ; => @[[1 2] [2 3] [3 4]] (sliding window)
(partition-all 2 [1 2 3]) ; => @[[1 2] [3]] (keeps the short tail)
(random-sample 0.5 (range 100)) ; keeps each item with probability 0.5
(interleave [:a :b :c] [1 2 3]) ; => @[:a 1 :b 2 :c 3]
;; Lazy filtering + transformation
(->> (range)
(filter even?)
(take 5)) ; => @[0 2 4 6 8]
;; Custom lazy sequence
(defn fibs []
(lazy-seq (cons 0 (cons 1
(map + (fibs) (rest (fibs)))))))
(doall (take 8 (fibs))) ; => [0 1 1 2 3 5 8 13]
(dorun (map println [1 2 3])) ; => nil (realize for side effects only)
(realized? (lazy-seq [1 2 3])) ; => false
```
Lazy file I/O:
```phel
(line-seq (php/fopen "file.txt" "r")) ; lazy line-by-line reading
(file-seq "src/") ; lazy recursive directory listing
(csv-seq (php/fopen "data.csv" "r")) ; lazy CSV parsing
(read-file-lazy "big.txt" 4096) ; lazy chunked reading
```
`map`, `filter`, `take`, `drop`, `concat`, `mapcat`, `interleave`, `partition` return lazy sequences.
## Threading macros
```phel
(-> {:name "Alice" :age 30} ; thread-first
(assoc :role "admin")
(dissoc :age)) ; => {:name "Alice", :role "admin"}
(->> [1 2 3 4 5] ; thread-last
(filter odd?)
(map inc)) ; => @[2 4 6]
(as-> [1 2 3] v ; thread with named binding
(conj v 4)
(count v)) ; => 4
(cond-> 1 ; conditional thread-first
true inc
false (* 42)) ; => 2
(cond->> [1 2 3] ; conditional thread-last
true (map inc)
false (filter odd?)) ; => @[2 3 4]
```
## Strings
```phel
(str "Hello" " " "World") ; => "Hello World"
(str "n=" 42) ; => "n=42"
(format "Hi %s, age %d" "Jo" 25) ; => "Hi Jo, age 25"
```
Requires `(:require phel.string :as str)`:
```phel
(ns my-app.strings
(:require phel.string :as str))
(str/lower-case "HELLO") ; => "hello"
(str/upper-case "hello") ; => "HELLO"
(str/replace "foo" "o" "0") ; => "f00"
(str/subs "hello" 1 3) ; => "el"
(str/split "a,b,c" #",") ; => ["a" "b" "c"] (Phel vector)
(str/join ", " ["a" "b" "c"]) ; => "a, b, c"
(str/starts-with? "hello" "he") ; => true
(str/ends-with? "hello" "lo") ; => true
(str/trim " hi ") ; => "hi"
(str/capitalize "hello world") ; => "Hello world"
(str/reverse "hello") ; => "olleh"
```
## Regular expressions
```phel
;; Regex literals use #"..." syntax (PCRE patterns)
(re-find #"\d+" "abc123def") ; => "123"
(re-find #"(\w+)@(\w+)" "user@host")
; => ["user@host" "user" "host"]
(re-matches #"\d+" "123") ; => "123"
(re-matches #"\d+" "abc123") ; => nil (must match entire string)
;; re-seq: all matches as a vector
(re-seq #"\d+" "a1b2c3") ; => ["1" "2" "3"]
;; Use regex for validation
(defn valid-email? [s]
(some? (re-matches #".+@.+\..+" s)))
(valid-email? "alice@example.com") ; => true
(valid-email? "not-an-email") ; => false
```
## Mutable state
```phel
(def counter (atom 0)) ; create an atom (mutable container)
(deref counter) ; => 0
@counter ; => 0 (shorthand for deref)
(reset! counter 42) ; direct reset
@counter ; => 42
(swap! counter inc) ; apply function, counter is now 43
(swap! counter + 10) ; counter is now 53
(compare-and-set! counter 53 100) ; => true (set only if current value matches)
(swap-vals! counter inc) ; => [100 101] (returns [old new])
(reset-vals! counter 0) ; => [101 0] (returns [old new])
;; Watchers: react to state changes
(add-watch counter :logger
(fn [key ref old-val new-val]
(println (str "Changed from " old-val " to " new-val))))
(remove-watch counter :logger)
;; Validators: constrain allowed values
(set-validator! counter #(>= % 0)) ; only non-negative values
(get-validator counter) ; => the validator fn
```
See [Global and Local Bindings](/documentation/language/global-and-local-bindings).
## Error handling
```phel
(try
(/ 1 0)
(catch DivisionByZeroError e
(str "Error: " (.getMessage e))))
(try
(do-risky-thing)
(catch Exception e
(println (str "Failed: " (.getMessage e))))
(finally
(cleanup)))
(throw (InvalidArgumentException. "bad input"))
;; Structured exceptions with ex-info
(throw (ex-info "User not found" {:id 42 :type :not-found}))
(try
(throw (ex-info "Validation failed" {:field :email} nil))
(catch Exception e
(ex-message e) ; => "Validation failed"
(ex-data e) ; => {:field :email}
(ex-cause e))) ; => nil
```
See [PHP Interop](/documentation/php-interop).
## Interfaces & structs
```phel
(definterface Greetable
(greet [this]))
(definterface HasArea
(area [this]))
(defstruct circle [radius]
HasArea
(area [this] (* 3.14159 radius radius)))
(defstruct person [name age]
Greetable
(greet [this] (str "Hello, I'm " name)))
(greet (person "Alice" 30)) ; => "Hello, I'm Alice"
(area (circle 5)) ; => 78.53975
(person? (person "Alice" 30)) ; => true
```
See [Interfaces](/documentation/language/interfaces).
## Protocols
Polymorphic dispatch on the first argument's type. More flexible than interfaces, extendable to existing types.
```phel
;; Define a protocol
(defprotocol Stringable
(to-string [this]))
(defstruct dog [name breed])
(extend-type dog
Stringable
(to-string [this] (str (:name this) " the " (:breed this))))
(to-string (dog "Rex" "Labrador")) ; => "Rex the Labrador"
;; Extend multiple types at once with extend-protocol
(extend-protocol Stringable
:string (to-string [this] this)
:int (to-string [this] (str this)))
;; Check protocol support
(satisfies? Stringable (dog "Rex" "Labrador")) ; => true
(extends? Stringable :string) ; => true
(extends? Stringable :array) ; => false
```
## Hierarchy system
Ad-hoc hierarchies for multimethods and `isa?`.
```phel
(derive :shape/square :shape/poly)
(derive :shape/circle :shape/poly)
(derive :shape/filled-square :shape/square)
(isa? :shape/square :shape/poly) ; => true
(isa? :shape/filled-square :shape/poly) ; => true
(parents :shape/square) ; => #{:shape/poly}
(ancestors :shape/filled-square) ; => #{:shape/square :shape/poly}
(descendants :shape/poly) ; => #{:shape/square :shape/circle :shape/filled-square}
(make-hierarchy) ; => {:parents {}, :descendants {}, :ancestors {}}
```
## Transducers
Composable transformations independent of the data source. Avoid intermediate collections.
```phel
;; Basic transducer usage with transduce
(transduce (map inc) + 0 [1 2 3]) ; => 9
(transduce (filter even?) + 0 [1 2 3 4]) ; => 6
;; Compose transducers (left-to-right order)
(def xf (comp (filter even?) (map inc)))
(transduce xf conj [] [1 2 3 4 5 6]) ; => [3 5 7]
;; into with a transducer (3-arg form)
(into [] (map inc) [1 2 3]) ; => [2 3 4]
(into #{} (filter odd?) [1 2 3 2 1]) ; => #{1 3}
;; sequence: lazy transducer application
(sequence (map inc) [1 2 3]) ; => [2 3 4]
;; cat: concatenating transducer for nested collections
(into [] cat [[1 2] [3 4] [5]]) ; => [1 2 3 4 5]
;; completing: supply a final step to a reducing function
(transduce (map inc) (completing + str) 0 [1 2 3]) ; => 9
;; Many core fns have transducer arities (called with no collection):
;; (map f), (filter pred), (take n), (drop n), (partition-all n), etc.
```
## PHP interop
```phel
;; Calling PHP functions
(php/strlen "test") ; => 4
(php/date "Y-m-d") ; => "2026-02-07"
(php/array_merge arr1 arr2) ; call any PHP function
;; Instantiation - all three forms are equivalent
(php/new DateTime "now")
(new DateTime "now")
(DateTime. "now") ; ClassName. shorthand (preferred)
;; Instance methods & properties
(php/-> obj (method arg)) ; $obj->method($arg)
(php/-> obj property) ; $obj->property
(php/-> obj (a) (b) (c)) ; chained: $obj->a()->b()->c()
(.method obj arg) ; shorthand
(.-property obj) ; property shorthand
;; Static methods & properties
(php/:: MyClass CONST) ; MyClass::CONST
(php/:: MyClass (create "x")) ; MyClass::create("x")
(MyClass/create "x") ; static shorthand
Ns.MyClass/CONST ; static member shorthand
;; PHP arrays
(php/aget arr 0) ; $arr[0] ?? null
(php/aset arr "k" "v") ; $arr["k"] = "v"
(php/apush arr "v") ; $arr[] = "v"
```
See [PHP Interop](/documentation/php-interop).
## Namespaces
```phel
(ns my-app.handlers
(:require my-app.db) ; import Phel module
(:require my-app.utils :as u) ; with alias
(:require my-app.auth :refer [login logout]) ; import symbols
(:use DateTimeImmutable) ; import PHP class
(:use Some.Long.Name :as Short)) ; PHP class with alias
(db/query "SELECT 1") ; use module prefix
(u/format-date date) ; use alias
(login credentials) ; use referred symbol
(DateTimeImmutable.) ; use imported class (ClassName. shorthand)
```
See [Namespaces](/documentation/language/namespaces).
## Testing
```phel
(ns my-app.tests
(:require phel.test :refer [deftest is are]))
(deftest addition-test
(is (= 4 (+ 2 2)))
(is (= 4 (+ 2 2)) "optional description"))
(deftest multiple-assertions
(are [expected input] (= expected (inc input))
2 1
3 2
4 3))
(deftest exception-test
(is (thrown? Exception
(throw (php/new Exception "boom")))))
```
```bash
./vendor/bin/phel test # run all tests
./vendor/bin/phel test tests/main.phel # run specific file
./vendor/bin/phel test --filter my-test # filter by name
./vendor/bin/phel test --fail-fast # stop on first failure
```
See [Testing](/documentation/testing).
## Async & concurrency
`async`, `await`, `await-all`, `await-any`, `->closure` are in `phel.core` (AMPHP-backed fibers).
```phel
;; Run body in a new fiber, returns an Amp\Future
(def f (async (+ 1 2)))
(await f) ; => 3 (blocks until resolved)
;; Await multiple futures concurrently
(await-all [(async 1) (async 2)]) ; => [1 2]
(await-any [(async 1) (async 2)]) ; => 1 (first to resolve)
;; Convert Phel fn to PHP Closure (for AMPHP and other libraries)
(->closure (fn [x] (* x 2)))
;; pmap: parallel map via fibers
(pmap inc [1 2 3]) ; => [2 3 4]
```
## Delay & force
`delay`, `delay?`, and `force` are in `phel.core` (auto-imported, no require needed). `phel.async/delay` is a different function that suspends a fiber for N seconds.
```phel
;; Delay defers evaluation until first access
(def d (delay (do (println "computing...") 42)))
(delay? d) ; => true
(force d) ; prints "computing...", => 42
(force d) ; => 42 (cached, no recomputation)
```
## Iteration
```phel
;; iteration: produce a lazy sequence from a step function
;; Useful for paginated APIs or stateful producers
(defn fetch-page [token]
{:items [1 2 3] :next-token (when (nil? token) "page2")})
(iteration fetch-page
{:kf :next-token
:vf :items
:initk nil})
```
## Arithmetic
```phel
(+ 1 2 3) ; => 6
(- 10 3) ; => 7
(* 2 3 4) ; => 24
(/ 10 2) ; => 5
(/ 10 3) ; => 10/3 (Ratio, exact)
(/ 10.0 3) ; => 3.333... (float)
(float (/ 10 3)) ; => 3.333... (coerce Ratio to float)
(quot 10 3) ; => 3 (integer quotient)
(rem 10 3) ; => 1 (remainder)
(mod -10 3) ; => 2 (modulo, always non-negative)
(** 2 10) ; => 1024
(bit-and 2r1100 2r1010) ; => 8
(bit-and-not 2r1111 2r0101) ; => 10 (and with the complement)
(bit-shift-right -8 1) ; => -4 (arithmetic, sign-preserving)
(unsigned-bit-shift-right -1 60) ; => 15 (logical, zero-fills)
```
Integer division (`/`) returns a `Ratio` when not evenly divisible. Use `float` or `(/ 10.0 3)` if you need a float.
## Utility functions
```phel
(parse-long "42") ; => 42
(parse-double "3.14") ; => 3.14
(parse-boolean "true") ; => true
(abs -5) ; => 5
(inf? php/INF) ; => true
(infinite? php/INF) ; => true (alias for inf?)
(nan? (php/log -1)) ; => true
(rational? 1/2) ; => true (integers, Ratio, BigDecimal)
(rational? 1.5) ; => false
(random-uuid) ; => "550e8400-e29b-..." (random UUID string)
```
## Printing
`print`/`println` render values for humans; the `pr` family renders them so the reader can read them back (strings quoted and escaped).
```phel
(pr-str "hi") ; => "\"hi\""
(prn-str [1 "a"]) ; => "[1 \"a\"]\n" (pr-str plus newline)
(println-str 1 2) ; => "1 2\n"
(str "hi") ; => "hi" (no quoting)
```
`pr` and `prn` write the same output to stdout instead of returning it.
## Serialization (EDN & Transit)
```phel
(ns my-app.serialize
(:require phel.edn :as edn)
(:require phel.transit :as transit))
;; phel.edn: eval-free EDN read/write (data only, no code execution)
(edn/read-string "{:a 1 :b [2 3]}") ; => {:a 1, :b [2 3]}
(edn/write-string {:a 1 :b [2 3]}) ; => "{:a 1, :b [2 3]}"
(edn/read-string-all "1 2 3") ; => [1 2 3] (every top-level form)
;; phel.transit: Transit + JSON-Verbose read/write
(transit/write-string {:a 1}) ; => "[\"~#cmap\",[\"~:a\",1]]"
(transit/read-string "[\"~:foo\",1]") ; => [:foo 1]
```
## Reflection
```phel
(ns my-app.introspect
(:require phel.reflect :as reflect))
;; phel.reflect: introspect PHP classes via reflection
(reflect/class-info \DateTime) ; => map of name, methods, properties, ...
(reflect/methods \DateInterval) ; => vector of method-info maps
(reflect/properties \DateInterval) ; => vector of property-info maps
(reflect/supers \RuntimeException) ; => parent classes + interfaces
```
## REPL utilities
```phel
(source my-fn) ; print source code of a function
(phel.repl/find-fn "map") ; search for functions by name
(symbol-info 'map) ; detailed info about a symbol
(ns-publics 'phel.core) ; all public vars in a namespace
(ns-aliases 'my-app.core) ; namespace aliases
(ns-refers 'my-app.core) ; referred symbols
(ns-list) ; list all loaded namespaces
(macroexpand-1 '(when true 1)) ; expand one level of macro
(macroexpand '(when true 1)) ; fully expand macro
(eval-str "(+ 1 2)") ; evaluate a string of Phel code
(load-file "src/my-module.phel") ; load and evaluate a file
(test-ns "my-app.tests") ; run tests in a namespace (name as string)
```
## Debugging
```phel
(dbg (* w h)) ; print [file:line] form => value to stderr, return value
(dbg) ; "reached here" marker, returns nil
(inspect x) ; print a structural view of any value
(break) ; pause and open a sub-REPL over the local bindings
(add-tap println) ; attach an inspector
(tap> {:event :login}) ; send a value to every tap (printed in the REPL by default)
;; phel.trace: log every call, including recursive ones, to stderr
(ns my-app.core (:require phel.trace :refer [deftrace dotrace]))
(deftrace fact [n] (if (< n 2) 1 (* n (fact (dec n)))))
(dotrace [parse-row normalize] (process-file "in.csv"))
```
See [Debugging](/documentation/debugging).
## Next steps
- [Getting Started](/documentation/getting-started): set up a project and a REPL.
- [Language section](/documentation/language/): the full reference behind each form here.
- [Agentic Coding](/documentation/reference/agentic-coding): truncation-safe rules for AI pairing.
---
# Basic Types
> Source: https://phel-lang.org/documentation/language/basic-types/
The building blocks of every Phel program: literals, numbers, strings, keywords, and the truthiness rules that differ from PHP.
## Nil, true, false
Literal constants:
```phel
nil
true
false
```
Only `false` and `nil` are falsy: `0`, `""`, and `[]` are all truthy (unlike PHP, where they are falsy). See [Truthiness](#truthiness) for the predicates and the full PHP comparison.
## Symbol
Names functions and variables:
```phel
symbol
snake_case_symbol
my-module/my-function
λ
```
## Keywords
Like a symbol but starts with `:`. Used as a constant. Interned, fast equality.
```phel
:keyword
:range
:0x0x0x
:a-keyword
::
```
Common as map keys:
```phel
;; Map with keyword keys
{:name "Alice" :email "alice@example.com"}
; Accessing map values with keywords
(get {:name "Alice" :age 30} :name) ; => "Alice"
(:name {:name "Alice" :age 30}) ; => "Alice" (keywords are functions!)
```
Like string constants, more efficient as map keys. Prefer over strings:
```phel
; Less idiomatic:
{"name" "Alice" "age" 30}
; Idiomatic:
{:name "Alice" :age 30}
```
Interned: one instance in memory, fast equality.
## Numbers
Integers, floats, ratios, big integers, big decimals. Integers and floats wrap PHP's natives. Integers in decimal, hex, octal, binary. Binary/octal/hex may use `_` separators.
```phel
1337 ; integer
+1337 ; positive integer
-1337 ; negative integer
1.234 ; float
+1.234 ; positive float
-1.234 ; negative float
1.2e3 ; float
7E-10 ; float
0b10100111001 ; binary number
+0b10100111001 ; positive binary number
-0b10100111001 ; negative binary number
0b101_0011_1001 ; binary number with underscores for better readability
0x539 ; hexadecimal number
+0x539 ; positive hexadecimal number
-0x539 ; negative hexadecimal number
-0x5_39 ; hexadecimal number with underscores
02471 ; octal number
+02471 ; positive octal number
-02471 ; negative octal number
024_71 ; octal number with underscores
```
### Ratios, BigInt, BigDecimal
```phel
1/2 ; Ratio
-3/4 ; Ratio
(/ 10 3) ; => 10/3 (int / int with non-integer result returns Ratio)
(numerator 1/2) ; => 1
(denominator 1/2) ; => 2
(bigint "100000000000000000000") ; BigInt from string
(bigint? 1N) ; predicate
1.5M ; BigDecimal literal (M suffix)
1.5e3M ; BigDecimal exponent
(bigdec "0.1")
(bigdec? 1.5M) ; => true
```
Auto-promoting variants `+'`, `-'`, `*'`, `inc'`, `dec'` widen to BigInt on overflow instead of wrapping.
## Arithmetic operators
Prefix notation:
```phel
;; 1 + (2*2) + (10/5) + 3 + 4 + (5 - 6)
(+ 1 (* 2 2) (/ 10 5) 3 4 (- 5 6)) ; => 13
```
Prefix notation (operator first) instead of PHP's infix:
```php
// PHP - infix notation
1 + (2 * 2) + (10 / 5) + 3 + 4 + (5 - 6);
// Phel - prefix notation
(+ 1 (* 2 2) (/ 10 5) 3 4 (- 5 6))
```
Operators take any number of args, no precedence concerns.
Operators take zero, one, or many args:
```phel
(+) ; => 0
(+ 1) ; => 1
(+ 1 2) ; => 3
(+ 1 2 3 4 5 6 7 8 9) ; => 45
(-) ; => 0
(- 1) ; => -1
(- 2 1) ; => 1
(- 3 2 1) ; => 0
(*) ; => 1
(* 2) ; => 2
(* 2 3 4) ; => 24
(/) ; => 1
(/ 2) ; => 1/2 (reciprocal as Ratio)
(/ 24 4 2) ; => 3
(/ 10 3) ; => 10/3 (Ratio, exact)
```
`(/ int int)` with a non-integer result returns a `Ratio`, not a float. Coerce with `float` or `(/ 10.0 3)` if you need a float.
Variadic operators are more flexible than PHP's:
```php
// PHP - requires at least two operands
1 + 2 + 3 + 4 + 5;
// Can't do this: +(); <- syntax error
// Phel - supports 0, 1, or many operands
(+) ; 0 (identity)
(+ 1) ; 1 (identity)
(+ 1 2 3 4 5) ; 15 (sum of all)
```
**Patterns:**
- `(+)` additive identity (0)
- `(*)` multiplicative identity (1)
- `(- x)` negate
- `(/ x)` reciprocal
Other numerics:
- `quot`, `rem`, `mod`: integer quotient, remainder, modulo. `%` aliases `rem`.
- `floor`, `ceil`, `round`, `sqrt`: math primitives.
- `**`: power.
- `+'`, `-'`, `*'`, `inc'`, `dec'`: auto-promote to `BigInt` on overflow.
- `numerator`, `denominator`, `rationalize`, `ratio?`.
- `bigint`, `biginteger`, `bigint?`; `bigdec`, `bigdec?` / `decimal?`.
Full list in the [API docs](/documentation/reference/api/core/).
Some operations yield NaN. Phel uses `NAN` constant. Check with `nan?`:
```phel
(nan? 1) ; false
(nan? (php/log -1)) ; true
(nan? NAN) ; true
```
NaN handling matches PHP:
```php
// PHP
is_nan(1); // false
is_nan(log(-1)); // true
is_nan(NAN); // true
// Phel
(nan? 1) ; false
(nan? (php/log -1)) ; true
(nan? NAN) ; true
```
`%` remainder and `**` exponent match PHP's.
## Bitwise operators
Manipulate bits in integers.
```phel
;; Bitwise and
(bit-and 0b1100 0b1001) ; => 8 (0b1000)
;; Bitwise or
(bit-or 0b1100 0b1001) ; => 13 (0b1101)
;; Bitwise xor
(bit-xor 0b1100 0b1001) ; => 5 (0b0101)
;; Bitwise complement
(bit-not 0b0111) ; => -8
;; Shifts bit n steps to the left
(bit-shift-left 0b1101 1) ; => 26 (0b11010)
;; Shifts bit n steps to the right
(bit-shift-right 0b1101 1) ; => 6 (0b0110)
;; Set bit at index n
(bit-set 0b1011 2) ; => 15 (0b1111)
;; Clear bit at index n
(bit-clear 0b1011 3) ; => 3 (0b0011)
;; Flip bit at index n
(bit-flip 0b1011 2) ; => 15 (0b1111)
;; Test bit at index n
(bit-test 0b1011 0) ; => true
(bit-test 0b1011 2) ; => false
```
Named functions instead of PHP operators:
```php
// PHP bitwise operators
0b1100 & 0b1001; // AND
0b1100 | 0b1001; // OR
0b1100 ^ 0b1001; // XOR
~0b0111; // NOT
0b1101 << 1; // Left shift
0b1101 >> 1; // Right shift
// Phel named functions
(bit-and 0b1100 0b1001)
(bit-or 0b1100 0b1001)
(bit-xor 0b1100 0b1001)
(bit-not 0b0111)
(bit-shift-left 0b1101 1)
(bit-shift-right 0b1101 1)
```
Adds extra functions not in PHP: `bit-set`, `bit-clear`, `bit-flip`, `bit-test`.
## Strings
Double-quoted. `$` doesn't need escaping.
```phel
"hello world"
"this is\na\nstring"
"this
is
a
string."
"use backslash to escape \" string"
"the dollar must not be escaped: $ or $abc just works"
"Hexadecimal notation is supported: \x41"
"Unicodes can be encoded: \u{1000}"
```
Concat and convert with `str`:
```phel
(str "Hello" " " "World") ; => "Hello World"
(str "The answer is " 42) ; => "The answer is 42"
```
Strings are iterable: work with `map`, `filter`, `count`, `frequencies`, `foreach`. Full UTF-8 / multibyte support:
```phel
(count "hello") ; => 5
(frequencies "abracadabra") ; => {a 5, b 2, r 2, c 1, d 1}
(seq "abc") ; => [a b c]
```
PHP strings internally. Use `phel.string` for idiomatic string operations:
```phel
(ns example
(:require phel.string :as str))
(count "hello") ; => 5
(str/upper-case "hello") ; => "HELLO"
(str/replace "hello" "o" "0") ; => "hell0"
```
All PHP string functions also available via `php/` prefix. Same as PHP double-quoted strings, except `$` doesn't need escaping.
## Lists
Whitespace-separated values in parentheses:
```phel
(do 1 2 3)
```
Lists are function/macro/special-form calls. Quoted lists are data:
```phel
'(1 2 3)
```
## Vectors
Whitespace-separated values in brackets:
```phel
[1 2 3] ; same as (vector 1 2 3)
```
Indexed data structure. Unlike PHP arrays, vectors are not maps/hashtables.
## Maps
Whitespace-separated key/value pairs in braces. Even count: key1, value1, key2, value2.
```phel
{} ; same as (hash-map)
{:key1 "value1" :key2 "value2"}
; Any type can be a key
{'(1 2 3) '(4 5 6)} ; Lists as keys
{[] []} ; Vectors as keys
{1 2 3 4 5 6} ; Numbers as keys
; Common pattern: keywords as keys
{:name "Alice" :age 30 :email "alice@example.com"}
```
Unlike PHP associative arrays, Phel map keys can be **any type** (vectors, lists, other maps), maps are **immutable** (operations return new maps), and they are **not** PHP arrays internally. Worked comparison in [Data structures → Immutability](/documentation/language/data-structures/#immutability-vs-php-mutability).
## Sets
Whitespace-separated values in `#{}`, or built with `hash-set`:
```phel
#{1 2 3} ; set literal
(hash-set 1 2 3) ; same result
(set [1 2 3]) ; coerce a collection to a set
```
## Queues
Persistent FIFO queues with amortised O(1) `conj`, `peek`, `pop`:
```phel
(def q (queue 1 2 3))
(queue? q) ; => true
(peek q) ; => 1
(conj q 4) ; => <-(1 2 3 4)-<
(pop q) ; => <-(2 3)-<
```
## Map entries
`map-entry` produces an entry that compares equal to a 2-element vector. `seq` over a map yields map entries:
```phel
(def e (map-entry :a 1))
(map-entry? e) ; => true
(key e) ; => :a
(val e) ; => 1
(= e [:a 1]) ; => true
```
## Tagged literals
Reader tags for common values:
```phel
#inst "2026-04-20T12:00:00Z" ; => \DateTimeImmutable
#regex "\\d+" ; => PCRE pattern string (delimited)
#uuid "550e8400-e29b-41d4-a716-446655440000"
```
### Custom tags
Register with `register-tag`:
```phel
(ns my-app.readers
(:require phel.reader :refer [register-tag]))
(register-tag "money" (fn [[amount currency]]
{:amount amount :currency currency}))
```
In any source file:
```phel
#money [100 "EUR"] ; => {:amount 100 :currency "EUR"}
```
A `data-readers.phel` at any source root auto-loads. Ship tag definitions with your library.
## PHP reader literals
Native PHP arrays inline without `php/array`:
```phel
#php [1 2 3] ; expands to (php-indexed-array 1 2 3)
#php {"a" 1 "b" 2} ; expands to (php-associative-array "a" 1 "b" 2)
```
Non-recursive expansion. Nested Phel forms stay Phel data.
## Regex literals
`#"..."` is reader sugar for PCRE patterns:
```phel
#"\d+" ; Matches one or more digits
#"[a-zA-Z]+" ; Matches one or more letters
#"hello\s+world" ; Matches "hello" followed by whitespace and "world"
```
Use with `re-find` and `re-matches`:
```phel
(re-find #"\d+" "abc123def") ; => "123"
(re-find #"\d+" "no digits") ; => nil
(re-matches #"\d+" "123") ; => "123" (full string must match)
(re-matches #"\d+" "abc123") ; => nil (not a full match)
; Capture groups return vectors
(re-find #"(\d+)-(\d+)" "date: 2026-04-03")
; => ["2026-04" "2026" "04"]
```
Same `#"..."` syntax as Clojure. Engine is PHP PCRE, not Java regex, so some details differ.
## Anonymous function shorthand
`#(...)` defines an inline anonymous function, using `%`/`%1`/`%2`/`%&` for positional arguments: `#(* % 2)` is the same as `(fn [x] (* x 2))`. Full rules, and the `|(...)` form removed in 0.50, live in [Functions and Recursion](/documentation/language/functions-and-recursion/#anonymous-function-fn).
## Deref shorthand
`@x` is shorthand for `(deref x)`, reading the current value of an atom or other reference type. Atom mechanics (`swap!`, `reset!`, the `!` convention) live in [Global and local bindings](/documentation/language/global-and-local-bindings/#atoms).
## Comments
`;` runs to end of line. `;;` for standalone, `;` for inline:
```phel
;; This is a standalone comment
(+ 1 2) ; This is an inline comment
```
> **Deprecation:** `#` line and `#| ... |#` multiline comments are deprecated. Use `;` and `;;`. `#` prefix is reserved for reader macros (`#()`, `#""`, `#?()`).
`#_` comments out the next form. Stack to comment multiple forms:
```phel
[:one :two :three] ; => [:one :two :three]
[#_:one :two :three] ; => [:two :three]
[#_:one :two #_:three] ; => [:two]
[#_#_:one :two :three] ; => [:three]
```
See [comment](/documentation/reference/api/core/#comment) macro: ignores forms, returns `nil`, still requires valid Phel code.
## Truthiness
Only `false` and `nil` are falsy. `truthy?` checks truthiness. `true?` and `false?` check for the exact values.
```phel
(truthy? false) ; => false
(truthy? nil) ; => false
(truthy? true) ; => true
(truthy? 0) ; => true
(truthy? -1) ; => true
(true? true) ; => true
(true? false) ; => false
(true? 0) ; => false
(false? false) ; => true
(false? true) ; => false
(false? 0) ; => false
```
This is **different from PHP** where `0`, `""`, `[]`, and `null` are all falsy.
```php
// PHP
if (0) { } // false - won't execute
if ("") { } // false - won't execute
if ([]) { } // false - won't execute
// Phel
(if 0 "yes" "no") ; => "yes" - 0 is truthy!
(if "" "yes" "no") ; => "yes" - "" is truthy!
(if [] "yes" "no") ; => "yes" - [] is truthy!
```
## Identity vs equality
`identical?` returns `true` if two values are identical. Stricter than equality: types match, then values. Keywords/symbols with same name always identical. Lists, vectors, maps, sets identical only if same reference.
```phel
(identical? true true) ; => true
(identical? true false) ; => false
(identical? 5 "5") ; => false
(identical? :test :test) ; => true
(identical? 'sym 'sym) ; => true
(identical? '() '()) ; => false
(identical? [] []) ; => false
(identical? {} {}) ; => false
```
`=` checks equality: same type and value. Collections equal if values match (no reference check).
```phel
(= true true) ; => true
(= 5 "5") ; => false
(= 5 5) ; => true
(= 5 5.0) ; => false
(= :test :test) ; => true
(= [] []) ; => true
(= {} {}) ; => true
```
Use `not=` for inequality.
- `identical?` like `===` (strict, with Phel types)
- `=` is **not** like `==` (loose equality)
- `=` compares structurally with type checking
PHP loose equality: use `php/==`:
```phel
(php/== 5 "5") ; => true (PHP loose equality)
(= 5 "5") ; => false (Phel structural equality)
(identical? 5 5) ; => true (Phel identity)
```
## Comparisons
All comparison operators accept multiple arguments:
```phel
(< 1 2) ; => true
(< 1 2 3) ; => true (1 < 2 and 2 < 3)
(< 1 3 2) ; => false (3 is not < 2)
(>= 5 5) ; => true
(> 3 2 1) ; => true (3 > 2 and 2 > 1)
```
## Logical operations
`and` evaluates left-to-right. Returns first falsy value or the last value. No args returns `true`.
```phel
(and) ; => true
(and 1) ; => 1
(and false) ; => false
(and true 5) ; => 5
```
`or` evaluates left-to-right. Returns first truthy value or the last value. No args returns `nil`.
```phel
(or) ; => nil
(or 1) ; => 1
(or false 5) ; => 5
```
`not` returns `true` for falsy values, `false` otherwise.
```phel
(not 1) ; => false
(not false) ; => true
(not nil) ; => true
```
## Next steps
- [Data structures](/documentation/language/data-structures/) - lists, vectors, maps, and sets in depth
- [Control flow](/documentation/language/control-flow/) - put truthiness to work with `if`, `cond`, and `case`
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Data structures
> Source: https://phel-lang.org/documentation/language/data-structures/
Phel's four core collections are lists, vectors, maps, and sets. All are **persistent** (immutable): an operation returns a new version that shares structure with the old one, and the original never changes.
"Copy-on-write" for collections. Prevents bugs from unexpected mutations.
## Lists
Linked list. Fast first-element access, slow random access. Lists are function/macro/special-form calls.
Create with `list` or by quoting a parenthesized form:
```phel
(list 1 2 3) ; use the list function to create a new list
'(1 2 3) ; use a quote to create a list
```
Access values with `get`, `first`, `second`, `next`, `rest`, `peek`:
```phel
(get (list 1 2 3) 0) ; Evaluates to 1
(first (list 1 2 3)) ; Evaluates to 1
(second (list 1 2 3)) ; Evaluates to 2
(peek (list 1 2 3)) ; Evaluates to 3
(next (list 1 2 3)) ; Evaluates to (2 3)
(next (list)) ; Evaluates to nil
(rest (list 1 2 3)) ; Evaluates to (2 3)
(rest (list)) ; Evaluates to ()
```
Add to the front with `cons`:
```phel
(cons 1 (list)) ; Evaluates to (1)
(cons 3 (list 1 2)) ; Evaluates to (3 1 2)
```
`count` for length:
```phel
(count (list)) ; Evaluates to 0
(count (list 1 2 3)) ; Evaluates to 3
```
## Vectors
Indexed, sequential. Fast random access by index, fast append at end.
Create with brackets, `vector`, or coerce with `vec`:
```phel
[1 2 3] ; Creates a new vector with three values
(vector 1 2) ; Creates a new vector with two values
(vec '(1 2 3)) ; Coerce a list to a vector: [1 2 3]
(vec #{1 2 3}) ; Coerce a set to a vector
```
`get` by index. `first`, `second`, `peek` for first/second/last:
```phel
(get [1 2 3] 0) ; Evaluates to 1
(first [1 2 3]) ; Evaluates to 1
(second [1 2 3]) ; Evaluates to 2
(peek [1 2 3]) ; Evaluates to 3
```
Append with `conj`:
```phel
(conj [1 2 3] 4) ; Evaluates to [1 2 3 4]
```
Change a value with `assoc`:
```phel
(assoc [1 2 3] 0 4) ; Evaluates to [4 2 3]
(assoc [1 2 3] 3 4) ; Evaluates to [1 2 3 4]
```
Length with `count`:
```phel
(count []) ; Evaluates to 0
(count [1 2 3]) ; Evaluates to 3
```
Like PHP indexed arrays (`[0 => 'a', 1 => 'b']`), but immutable.
## Maps
Key-value pairs in any order. Each key once. Any value implementing `HashableInterface` and `EqualsInterface` can be a key (vectors, lists, maps).
Create with braces or `hash-map`:
```phel
{:key1 "value1" :key2 "value2"} ; A new hash-map using shortcut syntax
(hash-map :key1 "value1" :key2 "value2") ; A new hash-map using the function
;; Any type can be a key
{[1 2] "vector-key" :keyword "keyword-key" "string" "string-key"}
```
Access with `get`:
```phel
(get {:a 1 :b 2} :a) ; Evaluates to 1
(get {:a 1 :b 2} :b) ; Evaluates to 2
(get {:a 1 :b 2} :c) ; Evaluates to nil
```
Add or update with `assoc`. Multiple pairs at once:
```phel
(assoc {} :a "hello") ; Evaluates to {:a "hello"}
(assoc {:a "foo"} :a "bar") ; Evaluates to {:a "bar"}
(assoc {} :a 1 :b 2 :c 3) ; Evaluates to {:a 1 :b 2 :c 3}
```
Remove with `dissoc`:
```phel
(dissoc {:a "foo"} :a) ; Evaluates to {}
```
`count` for size:
```phel
(count {}) ; Evaluates to 0
(count {:a "foo"}) ; Evaluates to 1
```
Like PHP associative arrays, but with two differences: keys can be **any type** (vectors, lists, other maps), and maps are **immutable**: "updating" with `assoc` returns a new map and leaves the original untouched. Worked comparison in [Immutability vs PHP mutability](#immutability-vs-php-mutability) below.
## Working with collections
Core functions span data structures.
### Adding with `conj`
`conj` adds elements. Behavior depends on type for efficiency:
```phel
;; Vectors - appends to end
(conj [1 2 3] 4) ; Evaluates to [1 2 3 4]
(conj [] 1 2 3) ; Evaluates to [1 2 3]
;; Sets - adds element
(conj #{1 2 3} 4) ; Evaluates to #{1 2 3 4}
(conj #{1 2 3} 2) ; Evaluates to #{1 2 3} (already present)
;; Lists - prepends to front (for efficiency)
(conj (list 1 2 3) 0) ; Evaluates to (0 1 2 3)
;; Maps - adds key-value pair
(conj {:a 1} [:b 2]) ; Evaluates to {:a 1 :b 2}
(conj {} [:a 1] [:b 2]) ; Evaluates to {:a 1 :b 2}
```
### Associating with `assoc`
`assoc` sets a key in maps, vectors (by index), structs:
```phel
;; Maps - set or update key-value pairs
(assoc {} :a "hello") ; Evaluates to {:a "hello"}
(assoc {:a "foo"} :a "bar") ; Evaluates to {:a "bar"}
(assoc {:a 1} :b 2 :c 3) ; Evaluates to {:a 1 :b 2 :c 3}
;; Vectors - set value at index (can extend by one position)
(assoc [1 2 3] 0 4) ; Evaluates to [4 2 3]
(assoc [1 2 3] 3 4) ; Evaluates to [1 2 3 4]
(assoc [] 0 "first") ; Evaluates to ["first"]
```
### Removing with `dissoc`
`dissoc` removes a key:
```phel
;; Maps - remove key-value pair
(dissoc {:a 1 :b 2} :a) ; Evaluates to {:b 2}
(dissoc {:a 1 :b 2 :c 3} :a :c) ; Evaluates to {:b 2}
;; Sets - remove element
(dissoc #{1 2 3} 2) ; Evaluates to #{1 3}
(dissoc #{1 2 3} 2 3) ; Evaluates to #{1}
```
### Nested operations
`-in` variants for nested structures:
```phel
;; get-in - Access nested values
(get-in {:a {:b {:c 1}}} [:a :b :c]) ; Evaluates to 1
(get-in {:users [{:name "Alice"}]} [:users 0 :name]) ; Evaluates to "Alice"
;; assoc-in - Set nested values
(assoc-in {} [:a :b :c] 1) ; Evaluates to {:a {:b {:c 1}}}
(assoc-in {:a {:b 1}} [:a :c] 2) ; Evaluates to {:a {:b 1 :c 2}}
;; update - Update a value by applying a function
(update {:a 1} :a inc) ; Evaluates to {:a 2}
(update [1 2 3] 0 + 10) ; Evaluates to [11 2 3]
;; update-in - Update nested values
(update-in {:a {:b 1}} [:a :b] inc) ; Evaluates to {:a {:b 2}}
```
### Immutability vs PHP mutability
```php
// PHP: Mutable operations
$users = ['Alice', 'Bob'];
$users[] = 'Charlie'; // $users is now ['Alice', 'Bob', 'Charlie']
echo $users[0]; // Still 'Alice'
// PHP: Mutating a map
$config = ['theme' => 'dark', 'lang' => 'en'];
$config['theme'] = 'light'; // Overwrites in place
```
```phel
;; Phel: Immutable operations
(def users ["Alice" "Bob"])
(def updated-users (conj users "Charlie")) ; New collection
;; users is still ["Alice" "Bob"]
;; updated-users is ["Alice" "Bob" "Charlie"]
;; Phel: Creating a new map
(def config {:theme "dark" :lang "en"})
(def new-config (assoc config :theme "light"))
;; config is still {:theme "dark" :lang "en"}
;; new-config is {:theme "light" :lang "en"}
```
**Why immutability matters:**
- **Thread-safe** reads
- **Predictable**: functions can't mutate your data
- **Time-travel**: keep old versions for undo/history
- **Easier debugging**: no surprise changes
**With PHP code:** use `php/aset` for mutable PHP arrays:
```phel
(def php-arr (php/array))
(php/aset php-arr "key" "value") ; Mutates the PHP array
```
### Clojure compatibility
Phel matches Clojure's names:
| Function | Behavior | Clojure Compatible? |
|-------------|-----------------------------|----------------------|
| `conj` | Add element (type-specific) | ✓ Yes |
| `assoc` | Associate key with value | ✓ Yes |
| `dissoc` | Dissociate key | ✓ Yes |
| `get` | Get value by key | ✓ Yes |
| `get-in` | Get nested value | ✓ Yes |
| `assoc-in` | Set nested value | ✓ Yes |
| `update` | Update with function | ✓ Yes |
| `update-in` | Update nested with function | ✓ Yes |
**Migration:** `push`, `put`, `unset` deprecated. Use `conj`, `assoc`, `dissoc`.
## Structs
A struct is a Map with a fixed set of keys and a global name. `defstruct` also defines a predicate function.
```phel
(defstruct my-struct [a b c]) ; Defines the struct
(let [x (my-struct 1 2 3)] ; Create a new struct
(my-struct? x) ; Evaluates to true
(get x :a) ; Evaluates to 1
(assoc x :a 12)) ; Evaluates to (my-struct 12 2 3)
```
Internally, Structs are PHP classes (one property per key). Faster than Maps. Every struct implements `\Countable`, `\ArrayAccess`, and `\IteratorAggregate`, so PHP code can `count($s)` and read fields by string offset (`$s['name']`) as well as by keyword.
Expose PHP magic methods (`__invoke`, `__toString`, `__get`, ...) through a `:php` block. The first arg binds to `$this`; read fields with `(get this :field)`.
```phel
(defstruct multiplier [factor]
:php
(__invoke [this x] (* x (get this :factor)))
(__toString [this] (str "x" (get this :factor))))
(let [m (multiplier 3)]
(m 14)) ; => 42 (PHP calls __invoke)
```
A `:php` block coexists with regular interface implementations. A custom `__invoke` must take exactly one call argument or be variadic (a struct is already callable as a key lookup), else the compiler rejects it.
## Sets
Unique values in any order. Values must implement `HashableInterface` and `EqualsInterface`.
Create with `#{}`, `hash-set`, or coerce with `set`:
```phel
#{1 2 3} ; A new set using shortcut syntax
(hash-set 1 2 3) ; A new set from individual arguments
(set [1 2 3]) ; Coerce a collection to a set
(set '(1 2 3)) ; Works with any collection type
```
> **Note:** `set` coerces a collection (Clojure alignment). `hash-set` builds from individual args.
Add with `conj`:
```phel
(conj #{1 2 3} 4) ; Evaluates to #{1 2 3 4}
(conj #{1 2 3} 2) ; Evaluates to #{1 2 3}
```
Remove with `dissoc`:
```phel
(dissoc #{1 2 3} 2) ; Evaluates to #{1 3}
```
Size with `count`:
```phel
(count #{}) ; Evaluates to 0
(count #{2}) ; Evaluates to 1
```
`union`: all elements of multiple sets.
```phel
(union) ; Evaluates to #{}
(union #{1 2}) ; Evaluates to #{1 2}
(union #{1 2} #{0 3}) ; Evaluates to #{0 1 2 3}
```
`intersection`: elements shared by all sets.
```phel
(intersection #{1 2} #{0 3}) ; Evaluates to #{}
(intersection #{1 2} #{0 1 2 3}) ; Evaluates to #{1 2}
```
`difference`: elements in first set not in the others.
```phel
(difference #{1 2} #{0 3}) ; Evaluates to #{1 2}
(difference #{1 2} #{0 1 2 3}) ; Evaluates to #{}
(difference #{0 1 2 3} #{1 2}) ; Evaluates to #{0 3}
```
`symmetric-difference`: elements in some sets but not in their intersection.
```phel
(symmetric-difference #{1 2} #{0 3}) ; Evaluates to #{0 1 2 3}
(symmetric-difference #{1 2} #{0 1 2 3}) ; Evaluates to #{0 3}
```
`subset?` and `superset?`:
```phel
(subset? (hash-set 1 2) (hash-set 1 2 3)) ; Evaluates to true
(subset? (hash-set 1 4) (hash-set 1 2 3)) ; Evaluates to false
(superset? (hash-set 1 2 3) (hash-set 1 2)) ; Evaluates to true
(superset? (hash-set 1 2 3) (hash-set 1 4)) ; Evaluates to false
```
## Transients
Most persistent structures have a transient (mutable) version (not lists). Same storage, but modifies in place.
Faster, used as builders. Conversion to/from persistent is cheap.
Convert a PHP array to a persistent map:
```phel
(defn php-array-to-map
"Converts a PHP Array to a map."
[arr]
(let [res (transient {})] ; Convert a persistent data to a transient
(foreach [k v arr]
(assoc res k v)) ; Fill the transient map (mutable)
(persistent res))) ; Convert the transient map to a persistent map.
```
## Data structures as functions
All data structures are callable:
```phel
((list 1 2 3) 0) ; Same as (get (list 1 2 3) 0)
([1 2 3] 0) ; Same as (get [1 2 3] 0)
({:a 1 :b 2} :a) ; Same as (get {:a 1 :b 2} :a)
(#{1 2 3} 1) ; Same as (get #{1 2 3} 1)
;; Practical use with map
(def users [{:name "Alice" :age 30}
{:name "Bob" :age 25}])
(map :name users) ; Evaluates to @["Alice" "Bob"]
```
## Example: working with user data
```phel
;; Start with user data
(def user {:id 1
:name "Alice"
:email "alice@example.com"
:settings {:theme "dark" :notifications true}})
;; Access nested data
(get-in user [:settings :theme]) ; => "dark"
;; Update nested settings immutably
(def updated-user
(assoc-in user [:settings :theme] "light"))
;; user still has "dark", updated-user has "light"
;; Add a new field
(def user-with-role
(assoc updated-user :role "admin"))
;; Update using a function
(def user-with-incremented-id
(update user-with-role :id inc))
;; Working with collections of users
(def users
[{:name "Alice" :active true}
{:name "Bob" :active false}
{:name "Charlie" :active true}])
;; Filter active users and get their names
(->> users
(filter :active) ; Keep only active users
(map :name) ; Extract names
(into #{})) ; Convert to a set
;; => #{"Alice" "Charlie"}
;; Build a map from a PHP array (common when interoping with PHP)
(defn php-response-to-map
"Convert a PHP API response to Phel data structures"
[php-arr]
(let [data (transient {})]
(foreach [k v php-arr]
(assoc data (keyword k) v))
(persistent data)))
;; Use with nested structures
(def api-response
(php/array "user_id" 123
"user_name" "Alice"
"is_active" true))
(php-response-to-map api-response)
;; => {:user_id 123 :user_name "Alice" :is_active true}
```
### Common patterns
**Building data incrementally:**
```phel
;; PHP way (mutable)
;; $result = [];
;; $result['id'] = 1;
;; $result['name'] = 'Alice';
;; return $result;
;; Phel way (immutable)
(-> {}
(assoc :id 1)
(assoc :name "Alice"))
;; Or all at once:
{:id 1 :name "Alice"}
```
**Updating deeply nested data:**
```phel
(def app-state
{:ui {:sidebar {:width 200 :visible true}}
:user {:name "Alice"}})
;; Change sidebar visibility
(assoc-in app-state [:ui :sidebar :visible] false)
;; Increment sidebar width
(update-in app-state [:ui :sidebar :width] + 50)
```
**Merging data:**
```phel
(def defaults {:theme "light" :lang "en" :debug false})
(def user-prefs {:theme "dark"})
(merge defaults user-prefs)
; => {:theme "dark" :lang "en" :debug false}
```
### Transforming map keys and values
`update-keys`, `update-vals` apply a function across keys/values:
```phel
; Transform all keys
(update-keys {:a 1 :b 2 :c 3} name)
; => {"a" 1 "b" 2 "c" 3}
(update-keys {"name" "Alice" "age" "30"} keyword)
; => {:name "Alice" :age "30"}
; Transform all values
(update-vals {:a 1 :b 2 :c 3} inc)
; => {:a 2 :b 3 :c 4}
(update-vals {:x "hello" :y "world"} phel.string/upper-case)
; => {:x "HELLO" :y "WORLD"}
```
### Building collections with `into`
`into` pours elements from one collection into another. Third arg applies a transducer:
```phel
; Two-argument form: pour elements into a collection
(into [] '(1 2 3)) ; => [1 2 3]
(into #{} [1 2 2 3 3]) ; => #{1 2 3}
(into {} [[:a 1] [:b 2]]) ; => {:a 1 :b 2}
; Three-argument form: apply a transducer during transfer
(into [] (map inc) [1 2 3]) ; => [2 3 4]
(into #{} (filter odd?) [1 2 3 4 5]) ; => #{1 3 5}
(into {} (map (fn [[k v]] [k (* v 2)])) (pairs {:a 1 :b 2}))
; => {:a 2 :b 4}
```
### Transducers
Composable transformations independent of context. `map`, `filter`, `remove`, `take`, `drop`, `take-while`, `drop-while`, `take-nth`, `keep`, `keep-indexed`, `distinct`, `dedupe`, `mapcat`, `interpose` return a transducer when called without a collection:
```phel
; Create a transducer by calling map/filter without a collection
(def xf (comp (filter odd?) (map #(* % 10))))
; Apply with transduce (reduces with a function)
(transduce xf + 0 [1 2 3 4 5]) ; => 90 (10 + 30 + 50)
; Apply with into (pours into a collection)
(into [] xf [1 2 3 4 5]) ; => [10 30 50]
; Apply with sequence (returns a lazy sequence)
(sequence xf [1 2 3 4 5]) ; => [10 30 50]
```
Common transducer producers:
```phel
(into [] (take 3) (range 10)) ; => [0 1 2]
(into [] (drop 7) (range 10)) ; => [7 8 9]
(into [] (take-while #(< % 5)) (range 10)) ; => [0 1 2 3 4]
(into [] (drop-while #(< % 5)) (range 10)) ; => [5 6 7 8 9]
(into [] (take-nth 3) (range 10)) ; => [0 3 6 9]
(into [] (distinct) [1 2 1 3 2 4]) ; => [1 2 3 4]
(into [] (dedupe) [1 1 2 2 3 1 1]) ; => [1 2 3 1]
(into [] (interpose :sep) [1 2 3]) ; => [1 :sep 2 :sep 3]
```
`completing` adapts a plain 2-arity reducing function into a full reducing function with 0-arity init and 1-arity completion (defaults to `identity`):
```phel
(def my-rf (completing conj))
(transduce (map inc) my-rf [1 2 3]) ; => [2 3 4]
```
`cat` concatenates inner collections:
```phel
(into [] cat [[1 2] [3 4] [5 6]]) ; => [1 2 3 4 5 6]
```
## Walking data structures
`phel.walk` recursively transforms nested data.
### walk
`walk` traverses a structure, applying `inner` to each element, then `outer` to the result:
```phel
(ns my-app
(:require phel.walk :refer [walk postwalk prewalk
postwalk-replace prewalk-replace
keywordize-keys stringify-keys]))
(walk inc identity [1 2 3]) ; => [2 3 4]
```
### postwalk and prewalk
`postwalk` applies bottom-up (children first). `prewalk` applies top-down:
```phel
(ns example
(:require phel.walk :refer [postwalk prewalk]))
;; Double every number in a nested structure
(postwalk #(if (number? %) (* % 2) %)
{:a 1 :b [2 3] :c {:d 4}})
;; => {:a 2 :b [4 6] :c {:d 8}}
;; prewalk visits parent before children
(prewalk #(if (number? %) (* % 2) %)
[1 [2 [3]]])
;; => [2 [4 [6]]]
```
### postwalk-replace and prewalk-replace
Replace values via map lookup:
```phel
(ns example
(:require phel.walk :refer [postwalk-replace]))
(postwalk-replace {:a :alpha :b :beta}
[:a {:b :c}])
;; => [:alpha {:beta :c}]
```
### keywordize-keys and stringify-keys
Convert map keys between keywords and strings. Useful for PHP arrays or JSON:
```phel
(ns example
(:require phel.walk :refer [keywordize-keys stringify-keys]))
(keywordize-keys {"name" "Alice" "age" 30})
;; => {:name "Alice" :age 30}
(stringify-keys {:name "Alice" :age 30})
;; => {"name" "Alice" "age" 30}
```
## Next steps
- [Destructuring](/documentation/language/destructuring/) - pull values out of collections by shape
- [Control flow](/documentation/language/control-flow/) - iterate and build collections with `for` and `loop`
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Functions and Recursion
> Source: https://phel-lang.org/documentation/language/functions-and-recursion/
Define and compose behavior: anonymous and named functions, multiple arities, tail-safe recursion with `recur`, and runtime polymorphism with multimethods.
## Anonymous function (fn)
```phel
(fn [params*] expr*)
(fn
([params1*] expr1*)
([params2*] expr2*)
...)
```
Defines a function: parameter list, expression list. Returns last expression's value. Earlier expressions evaluate for side-effects. No expressions returns `nil`.
Functions can have multiple arities. Call dispatches on argument count. At most one variadic clause, which must have the most params. No matching arity raises a clear compile/runtime error.
Functions introduce their own lexical scope.
```phel
(fn []) ; Function with no arguments that returns nil
(fn [x] x) ; The identity function
(fn [] 1 2 3) ; A function that returns 3
(fn [a b] (+ a b)) ; A function that returns the sum of a and b
```
Variadic functions use `&`:
```phel
(fn [& args] (count args)) ; A variadic function that counts the arguments
(fn [a b c &]) ; A variadic function with extra arguments ignored
(fn ; A multi-arity function
([] "hi")
([name] (str "hi " name))
([greeting name & rest] (str greeting " " name rest)))
```
Shorter form omits the parameter list, naming params by position:
* `%` or `%1` refers to the first argument
* `%2`, `%3`, etc. refer to subsequent arguments
* `%&` captures remaining variadic arguments
```phel
#(+ 6 %) ; Same as (fn [x] (+ 6 x))
#(+ %1 %2) ; Same as (fn [a b] (+ a b))
#(apply + %&) ; Same as (fn [& xs] (apply + xs))
; Using with higher-order functions
(map #(* % 2) [1 2 3]) ; => @[2 4 6]
(filter #(> % 3) [1 5 2 8]) ; => @[5 8]
```
> **Removed in 0.50:** `|(...)` with `$` / `$1` / `$&`. Use `#(...)` with `%` (matches Clojure).
`#()` short-form is like PHP arrow functions:
```php
// PHP
$add = fn($x) => $x + 6;
array_map(fn($x) => $x * 2, $array);
// Phel
(def add #(+ % 6))
(map #(* % 2) array)
```
## Global functions
```phel
(defn name docstring? attributes? [params*] expr*)
(defn name docstring? attributes?
([params1*] expr1*)
([params2*] expr2*)
...)
```
`defn` defines a global function. Multiple arities allowed; single variadic clause must declare the max arg count.
```phel
(defn my-add-function [a b]
(+ a b))
(defn greet
([] "hi")
([name] (str "hi " name))
([greeting name] (str greeting " " name)))
```
Optional doc string and attribute map:
```phel
(defn my-add-function
"adds value a and b"
[a b]
(+ a b))
```
### Private functions
Private functions don't export from the namespace. Two forms:
1. `{:private true}` attribute
2. `defn-` shorthand
```phel
(defn my-private-add-function
{:private true}
[a b]
(+ a b))
(defn- my-private-add-function
[a b]
(+ a b))
```
Equivalent, but `defn-` is more concise.
### Defn metadata shortcuts
Tag a `defn` with metadata to wrap the body automatically:
```phel
;; Memoize results - keep every (args -> value) pair forever
(defn ^:memoize fib [n]
(if (< n 2) n (+ (fib (dec n)) (fib (- n 2)))))
;; LRU cap of 128 entries
(defn ^{:memoize-lru 128} expensive [k]
(slow-lookup k))
;; Wrap body in (async ...) - returns Amp\Future
(defn ^:async fetch [url]
(http/get url))
```
`^:memoize` / `^{:memoize-lru N}` desugar to [`memoize`](/documentation/reference/api/core/#memoize) / [`memoize-lru`](/documentation/reference/api/core/#memoize-lru) wrappers; entries from recursive self-calls within a single invocation are retained. `^:async` wraps the body with `async`, returning an `Amp\Future`.
### Return and parameter types (`:tag`)
Annotate types with `:tag` metadata. The compiler emits PHP type declarations and runs static checks at compile time:
```phel
(defn ^int add [^int a ^int b] (+ a b))
(defn greet ^{:tag "?string"} [^string name]
(when (seq name) (str "hi " name)))
(defn make-foo ^"\\My\\Foo" [] (php/new "My\\Foo"))
```
Reader shorthands: `^int`, `^"?int"`, `^"\\Foo\\Bar"`, `^{:tag "..."}`.
Tag inference fills in return types from tail primitive ops, tail calls to tagged globals or pure PHP builtins, and parameter types from primitive body uses - inferred tags persist in def metadata and graft onto compiled PHP signatures for single-arity `defn`. Mismatches surface at compile time.
## Recursion
Like `loop`, functions can recurse with `recur`. TCO prevents stack overflow.
```phel
;; Recursive factorial (regular recursion - can stack overflow)
(defn factorial [n]
(if (<= n 1)
1
(* n (factorial (dec n)))))
(factorial 5) ; => 120
;; Tail-recursive factorial using recur with loop
(defn factorial-recur [n]
(loop [acc 1
n n]
(if (<= n 1)
acc
(recur (* acc n) (dec n)))))
(factorial-recur 5) ; => 120
;; Recursive sum (can stack overflow on large collections)
(defn sum-recursive [coll]
(if (empty? coll)
0
(+ (first coll) (sum-recursive (rest coll)))))
(sum-recursive [1 2 3 4 5]) ; => 15
;; Tail-recursive sum using recur (safe for large collections)
(defn sum-recur [coll]
(loop [acc 0
remaining coll]
(if (empty? remaining)
acc
(recur (+ acc (first remaining)) (rest remaining)))))
(sum-recur [1 2 3 4 5]) ; => 15
;; Using recur directly in function (also tail-call optimized)
(defn countdown [n]
(if (<= n 0)
"Done!"
(do
(println n)
(recur (dec n)))))
;; (countdown 5) ; Prints: 5, 4, 3, 2, 1, then returns "Done!"
```
`recur` compiles to a PHP `while`, avoiding "Maximum function nesting level" errors:
```php
// PHP - This will cause stack overflow for large n
function factorial($n) {
if ($n <= 1) return 1;
return $n * factorial($n - 1); // Stack overflow for large n!
}
// Phel with recur - This works for any size n
(defn factorial-recur [n]
(loop [acc 1
n n]
(if (<= n 1)
acc
(recur (* acc n) (dec n)))))
```
**Difference:** Recursion builds the call stack; `recur` reuses one stack frame (TCO).
## Multimethods
Runtime polymorphism via dispatch functions. Decouples dispatch from implementations, enabling open extension.
### Defining
`defmulti` declares the dispatch function. `defmethod` adds implementations per dispatch value:
```phel
;; Define a multimethod that dispatches on the :shape key
(defmulti area :shape)
;; Implement for each shape type
(defmethod area :circle [{:radius r}]
(* 3.14159 r r))
(defmethod area :rectangle [{:width w :height h}]
(* w h))
(defmethod area :triangle [{:base b :height h}]
(/ (* b h) 2))
(area {:shape :circle :radius 5}) ; => 78.53975
(area {:shape :rectangle :width 4 :height 3}) ; => 12
(area {:shape :triangle :base 6 :height 4}) ; => 12
```
### Custom dispatch
Dispatch function can be anything, not just a keyword:
```phel
(defmulti greeting #(get % :language))
(defmethod greeting "en" [_] "Hello!")
(defmethod greeting "es" [_] "Hola!")
(defmethod greeting "de" [_] "Hallo!")
(greeting {:language "es"}) ; => "Hola!"
```
## Apply functions
```phel
(apply f expr*)
```
Calls `f` with the args. Last arg must be a list, spread as separate arguments. Returns the result.
```phel
(apply + [1 2 3]) ; Evaluates to 6
(apply + 1 2 [3]) ; Evaluates to 6
```
`(apply + 1 2 3)` is invalid: last arg must be a list.
## Passing by reference
Pass a variable by reference with `:reference` metadata:
```phel
(fn [^:reference my-arr]
(php/apush my-arr 10))
```
Limited support: works for function arguments only (no destructuring).
Equivalent to PHP `&`:
```php
// PHP
function addToArray(&$arr) {
$arr[] = 10;
}
// Phel
(defn add-to-array [^:reference arr]
(php/apush arr 10))
```
**Note:** Prefer immutable data structures over mutating PHP arrays.
## Next steps
- [Destructuring](/documentation/language/destructuring/) - bind function params by shape
- [Macros](/documentation/language/macros/) - go beyond functions with compile-time code
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Control flow
> Source: https://phel-lang.org/documentation/language/control-flow/
Everything that decides what runs next: conditionals (`if`, `cond`, `case`), iteration (`loop`/`recur`, `foreach`, `for`), and conditional threading.
## If
```phel
(if test then else?)
```
Evaluates _test_. If truthy, returns _then_; if falsy, returns _else_ (or `nil`).
Only `false` and `nil` are falsy. Everything else truthy. PHP equivalent: `test !== null && test !== false`.
```phel
;; Basic if examples
(if true 10) ; Evaluates to 10
(if false 10) ; Evaluates to nil
(if true (print 1) (print 2)) ; Prints 1 but not 2
;; Important: Only false and nil are falsy!
(if 0 (print 1) (print 2)) ; Prints 1 (0 is truthy!)
(if nil (print 1) (print 2)) ; Prints 2 (nil is falsy)
(if [] (print 1) (print 2)) ; Prints 1 (empty vector is truthy!)
;; Practical examples
(defn greet [name]
(if name
(str "Hello, " name)
"Hello, stranger"))
(greet "Alice") ; => "Hello, Alice"
(greet nil) ; => "Hello, stranger"
;; Using if for validation
(defn divide [a b]
(if (= b 0)
nil
(/ a b)))
(divide 10 2) ; => 5
(divide 10 0) ; => nil
```
## Case
```phel
(case test & pairs)
```
Evaluates _test_, matches against first item of each pair. Returns the matching second item, or `nil` if no match.
```phel
;; Basic case examples
(case (+ 7 5)
3 :small
12 :big) ; Evaluates to :big
(case (+ 7 5)
3 :small
15 :big) ; Evaluates to nil (no match)
(case (+ 7 5)) ; Evaluates to nil (no pairs)
;; Practical examples
(defn http-status-message [code]
(case code
200 "OK"
201 "Created"
400 "Bad Request"
404 "Not Found"
500 "Internal Server Error"))
(http-status-message 200) ; => "OK"
(http-status-message 404) ; => "Not Found"
(http-status-message 999) ; => nil
;; Using case with keywords
(defn animal-sound [animal]
(case animal
:dog "Woof!"
:cat "Meow!"
:cow "Moo!"
:duck "Quack!"))
(animal-sound :dog) ; => "Woof!"
(animal-sound :fish) ; => nil
```
Like PHP `switch`, more concise:
```php
// PHP
switch ($value) {
case 3:
$result = 'small';
break;
case 12:
$result = 'big';
break;
default:
$result = null;
}
// Phel
(case value
3 :small
12 :big)
```
No `break`, no fall-through.
## Cond
```phel
(cond & pairs)
```
Walks pairs. First pair whose test is truthy: returns its second expression. No match returns `nil`.
```phel
;; Basic cond examples
(cond
(neg? 5) :negative
(pos? 5) :positive) ; Evaluates to :positive
(cond
(neg? 5) :negative
(neg? 3) :negative) ; Evaluates to nil (no match)
(cond) ; Evaluates to nil (no pairs)
;; Practical examples
(defn classify-number [n]
(cond
(< n 0) "negative"
(= n 0) "zero"
(> n 0) "positive"))
(classify-number -5) ; => "negative"
(classify-number 0) ; => "zero"
(classify-number 10) ; => "positive"
;; Using cond for complex conditions
(defn ticket-price [age]
(cond
(< age 3) 0 ; Free for toddlers
(< age 12) 5 ; Child price
(< age 65) 10 ; Adult price
:else 7)) ; Senior discount
(ticket-price 2) ; => 0
(ticket-price 10) ; => 5
(ticket-price 30) ; => 10
(ticket-price 70) ; => 7
;; Combining multiple conditions
(defn water-state [temp]
(cond
(<= temp 0) :ice
(and (> temp 0) (< temp 100)) :liquid
(>= temp 100) :steam))
(water-state -5) ; => :ice
(water-state 25) ; => :liquid
(water-state 105) ; => :steam
```
Like a chain of `if`/`elseif`:
```php
// PHP
if ($value < 0) {
$result = 'negative';
} elseif ($value > 0) {
$result = 'positive';
} else {
$result = null;
}
// Phel
(cond
(neg? value) :negative
(pos? value) :positive)
```
Cleaner than nested `if`. Use `:else` as a default.
For destructuring-by-shape (matching the structure of vectors and maps, not just running predicates), see [Match](#match) below.
## Match
`match` lives in `phel.match` and dispatches by _shape_: it destructures the subject and binds names in one step. It expands to nested `cond` + `let` at compile time, so there is no runtime overhead beyond the checks you write.
```phel
(ns my-app.main (:require phel.match :refer [match]))
(defn describe [x]
(match [x]
[0] "zero"
[[a b]] (str "pair " a " / " b)
[{:type :err :msg m}] (str "error: " m)
[(n :guard pos?)] "positive"
:else "other"))
(describe 0) ; => "zero"
(describe [1 2]) ; => "pair 1 / 2"
(describe {:type :err :msg "boom"}) ; => "error: boom"
(describe 5) ; => "positive"
```
The subject is a vector of one or more targets; every pattern is a vector whose length must equal the target count.
### Pattern kinds
| Pattern | Matches |
| --- | --- |
| `42`, `:key`, `"s"` | literal equality |
| `_` | wildcard (matches anything, binds nothing) |
| `sym` | binds the target to `sym` |
| `[a b c]` | a vector of exactly 3 elements, recursively matched |
| `[head & tail]` | a vector, binding the remaining slice to `tail` |
| `{:k sym}` | a map with key `:k`, binding its value to `sym` |
| `(pat :as name)` | matches `pat`, also binds the whole subject to `name` |
| `(pat :guard pred)` | matches `pat`, then requires `(pred subject)` truthy |
| `(:or alt1 alt2 ...)` | any alternative matches (literal/structural only, no bindings) |
### Guards
A `:guard` adds a runtime predicate on top of a structural pattern:
```phel
(ns my-app.main (:require phel.match :refer [match]))
(defn sign [n]
(match [n]
[(x :guard neg?)] "negative"
[(x :guard pos?)] "positive"
:else "zero"))
(sign -3) ; => "negative"
(sign 7) ; => "positive"
(sign 0) ; => "zero"
```
### Rest binding
End a vector pattern with `& rest` to capture the remaining slice:
```phel
(ns my-app.main (:require phel.match :refer [match]))
(match [[10 20 30]]
[[head & tail]] (str head ":" (count tail))) ; => "10:2"
```
### Pitfalls
* Each pattern vector's length must equal the target count.
* `:else` must be the final clause.
* `:or` alternatives may not introduce bindings; they are literal or structural only.
* Nested patterns bind left-to-right; a later binding shadows an earlier one with the same name.
* A `:guard` predicate runs against the raw value. Numeric predicates coerce non-numbers, so `(pos? [1 2])` is truthy. Put literal and structural patterns _before_ an open numeric guard.
See also [`phel.schema`](/documentation/reference/api/schema/) for shapes reusable across validation and matching, and `case`/`cond`/`condp` above for simpler dispatch without destructuring. Full API: [match reference](/documentation/reference/api/match/).
## Loop
```phel
(loop [bindings*] expr*)
```
Creates a lexical context with bindings and a recursion point at the top.
```phel
(recur expr*)
```
Evaluates expressions and rebinds at the recursion point. Recursion point is a `fn` or `loop`. Arities must match exactly.
`recur` compiles to a PHP `while` loop, avoiding _Maximum function nesting level_ errors. Using `recur` for tail-recursive functions and the tail-call story are covered in [Functions and Recursion](/documentation/language/functions-and-recursion/#recursion).
```phel
;; Basic loop example - sum numbers from 1 to 10
(loop [sum 0
cnt 10]
(if (= cnt 0)
sum
(recur (+ cnt sum) (dec cnt)))) ; => 55
;; Finding an element in a vector
(defn find-index [pred coll]
(loop [idx 0
items coll]
(cond
(empty? items) nil
(pred (first items)) idx
:else (recur (inc idx) (rest items)))))
(find-index even? [1 3 5 8 9]) ; => 3
(find-index neg? [1 2 3]) ; => nil
;; Building a result with loop
(defn reverse-vec [v]
(loop [result []
remaining v]
(if (empty? remaining)
result
(recur (conj result (last remaining))
(pop remaining)))))
(reverse-vec [1 2 3 4]) ; => [4 3 2 1]
```
## Foreach
```phel
(foreach [value valueExpr] expr*)
(foreach [key value valueExpr] expr*)
```
Iterate any PHP data structure for side-effects. Always returns `nil`. Prefer `loop` when possible.
```phel
(foreach [v [1 2 3]]
(print v)) ; Prints 1, 2 and 3
(foreach [k v {"a" 1 "b" 2}]
(print k)
(print v)) ; Prints "a", 1, "b" and 2
```
Mirrors PHP `foreach`:
```php
// PHP
foreach ([1, 2, 3] as $v) {
print($v);
}
foreach (["a" => 1, "b" => 2] as $k => $v) {
print($k);
print($v);
}
// Phel
(foreach [v [1 2 3]]
(print v))
(foreach [k v {"a" 1 "b" 2}]
(print k)
(print v))
```
**Note:** Use `for` or `loop` to return values. `foreach` is side-effects only.
## For
`for` builds collections from existing ones. Combines `foreach`, `let`, `if`, `reduce`.
```phel
(for head body+)
```
`head` is a vector of bindings and modifiers. A binding is `binding :verb expr` where `binding` works as in `let` and `:verb` is one of:
* `:range` loop over a range
* `:in` values of a collection
* `:keys` keys/indexes of a collection
* `:pairs` key-value pairs
Modifiers (form `:modifier argument`):
* `:while` break when expression is falsy
* `:let` additional bindings
* `:when` evaluate body only when condition is true
* `:reduce [acc init]` reduce instead of returning a list. `acc` starts at `init`. Unlike `when` inside `reduce`, `:when` works cleanly with `:reduce`
```phel
(for [x :range [0 3]] x) ; Evaluates to [0 1 2]
(for [x :range [3 0 -1]] x) ; Evaluates to [3 2 1]
(for [x :in [1 2 3]] (inc x)) ; Evaluates to [2 3 4]
(for [x :in {:a 1 :b 2 :c 3}] x) ; Evaluates to [1 2 3]
(for [x :keys [1 2 3]] x) ; Evaluates to [0 1 2]
(for [x :keys {:a 1 :b 2 :c 3}] x) ; Evaluates to [:a :b :c]
(for [[k v] :pairs {:a 1 :b 2 :c 3}] [v k]) ; Evaluates to [[1 :a] [2 :b] [3 :c]]
(for [[k v] :pairs [1 2 3]] [k v]) ; Evaluates to [[0 1] [1 2] [2 3]]
(for [[k v] :pairs {:a 1 :b 2 :c 3} :reduce [m {}]]
(assoc m k (inc v))) ; Evaluates to {:a 2, :b 3, :c 4}
(for [[k v] :pairs {:a 1 :b 2 :c 3} :reduce [m {}] :let [x (inc v)]]
(assoc m k x)) ; Evaluates to {:a 2, :b 3, :c 4}
(for [[k v] :pairs {:a 1 :b 2 :c 3} :when (contains-value? [:a :c] k) :reduce [acc {}]]
(assoc acc k v)) ; Evaluates to {:a 1, :c 3}
(for [x :in [2 2 2 3 3 4 5 6 6] :while (even? x)] x) ; Evaluates to [2 2 2]
(for [x :in [2 2 2 3 3 4 5 6 6] :when (even? x)] x) ; Evaluates to [2 2 2 4 6 6]
(for [x :in [1 2 3] :let [y (inc x)]] [x y]) ; Evaluates to [[1 2] [2 3] [3 4]]
(for [x :range [0 4] y :range [0 x]] [x y]) ; Evaluates to [[1 0] [2 0] [2 1] [3 0] [3 1] [3 2]]
```
List comprehension, not PHP's `for`:
```php
// PHP - manual array building
$result = [];
foreach (range(1, 3) as $x) {
$result[] = $x + 1;
}
// Phel - declarative comprehension
(for [x :in [1 2 3]] (inc x)) ; [2 3 4]
```
Combines iteration, filtering (`:when`), early termination (`:while`), reduction (`:reduce`), nested loops.
Like Clojure `for` (`:let`, `:when`, nesting). `:reduce` is a Phel extension.
## Do
```phel
(do expr*)
```
Evaluates expressions in order. Returns the last value, or `nil` if empty.
```phel
(do 1 2 3 4) ; Evaluates to 4
(do (print 1) (print 2) (print 3)) ; Print 1, 2, and 3
```
## Dofor
Like `for` but for side-effects. Returns `nil` like `foreach`.
```phel
(dofor [x :in [1 2 3]] (print x)) ; Prints 1, 2, 3, returns nil
(dofor [x :in [2 3 4 5] :when (even? x)] (print x)) ; Prints 2, 4, returns nil
```
## Conditional threading
### cond->
```phel
(cond-> expr & clauses)
```
Threads expression through each form whose test is truthy (thread-first). Skips forms with falsy tests.
```phel
(cond-> 1
true inc
false (* 42)
true (* 3)) ; => 6
;; Only applies inc (true) and (* 3) (true), skips (* 42) (false)
;; 1 -> (inc 1) -> 2 -> (* 2 3) -> 6
(defn maybe-transform [data opts]
(cond-> data
(:uppercase opts) (phel.string/upper-case)
(:trim opts) (phel.string/trim)
(:prefix opts) (#(str (:prefix opts) %))))
```
### cond->>
```phel
(cond->> expr & clauses)
```
Like `cond->` but threads as last arg (thread-last).
```phel
(cond->> [1 2 3 4 5]
true (map inc)
false (filter odd?)
true (take 3)) ; => @[2 3 4]
;; Only applies (map inc) and (take 3), skips (filter odd?)
```
## Exceptions
```phel
(throw expr)
```
Evaluates _expr_ and throws it. Must implement PHP `Throwable`.
## Try, catch, and finally
```phel
(try expr* catch-clause* finally-clause?)
```
Evaluates expressions. No exception: returns last value. Matching _catch-clause_: returns its value. No match: exception propagates. _finally-clause_ runs before return.
```phel
(try) ; Evaluates to nil
(try
(throw (Exception.))
(catch Exception e "error")) ; Evaluates to "error"
(try
(+ 1 1)
(finally (print "test"))) ; Evaluates to 2 and prints "test"
(try
(throw (Exception.))
(catch Exception e "error")
(finally (print "test"))) ; Evaluates to "error" and prints "test"
```
For catching PHP exceptions, structured errors with `ex-info`/`ex-data`, exception chaining, and guidance on when to throw, see [Error handling](/documentation/language/error-handling/).
## Next steps
- [Match reference](/documentation/reference/api/match/) - all `match` pattern kinds and the full API
- [Error handling](/documentation/language/error-handling/) - throw, catch, and structured errors in depth
- [Functions and recursion](/documentation/language/functions-and-recursion/) - `loop`/`recur` and tail calls
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Destructuring
> Source: https://phel-lang.org/documentation/language/destructuring/
Destructuring binds names to values inside data structures. Describe the shape, Phel binds the pieces.
Works in `let`, function params (`defn`, `fn`), `loop`.
## Sequential
Extract from vectors/lists by position with vector syntax:
```phel
(let [[a b] [1 2]]
(+ a b)) ; => 3
```
### Nested
Patterns nest arbitrarily deep:
```phel
(let [[a [b c]] [1 [2 3]]]
(+ a b c)) ; => 6
```
### Skipping
`_` ignores a position:
```phel
(let [[a _ b] [1 2 3]]
(+ a b)) ; => 4
```
### Rest args
`&` captures the remaining elements:
```phel
(let [[a b & rest] [1 2 3 4 5]]
rest) ; => [3 4 5]
```
More powerful than PHP `list()` or array unpacking:
```php
// PHP - limited destructuring
[$a, $b] = [1, 2];
['a' => $x, 'b' => $y] = ['a' => 1, 'b' => 2];
// Phel - full destructuring with nesting and rest
(let [[a [b c] & rest] [1 [2 3] 4 5 6]]
// a = 1, b = 2, c = 3, rest = [4 5 6]
)
```
Works in more places (function params, let, loop) with more patterns.
## Associative
Extract from maps by key with map syntax:
```phel
(let [{:a a :b b} {:a 1 :b 2}]
(+ a b)) ; => 3
```
### Nested associative
Mix map and vector patterns:
```phel
(let [{:a [a b] :c c} {:a [1 2] :c 3}]
(+ a b c)) ; => 6
```
### Defaults with `:or`
Defaults for missing keys:
```phel
(let [{:name name :role role :or {role "guest"}}
{:name "Alice"}]
(str name " (" role ")")) ; => "Alice (guest)"
```
Without `:or`, missing keys bind to `nil`.
Extract values by key:
```php
// PHP - manual extraction with defaults
$data = ['name' => 'Alice'];
$name = $data['name'];
$role = $data['role'] ?? 'guest';
// Phel - destructuring with :or
(let [{:name name :role role :or {role "guest"}}
{:name "Alice"}]
// name = "Alice", role = "guest"
)
```
## Index-based
Destructure vectors by index using map syntax:
```phel
(let [{0 a 1 b} [1 2]]
(+ a b)) ; => 3
(let [{0 [a b] 1 c} [[1 2] 3]]
(+ a b c)) ; => 6
```
Useful for specific positions in a large vector.
## In function parameters
Works directly in `defn` and `fn` params:
```phel
(defn greet [{:name name :role role :or {role "member"}}]
(str "Hello " name " (" role ")"))
(greet {:name "Alice" :role "admin"}) ; => "Hello Alice (admin)"
(greet {:name "Bob"}) ; => "Hello Bob (member)"
```
Sequential in params:
```phel
(defn distance [[x1 y1] [x2 y2]]
(php/sqrt (+ (* (- x2 x1) (- x2 x1))
(* (- y2 y1) (- y2 y1)))))
(distance [0 0] [3 4]) ; => 5
```
## In `loop`
Loop bindings:
```phel
(loop [[head & tail] [1 2 3 4 5]
acc 0]
(if (nil? head)
acc
(recur tail (+ acc head)))) ; => 15
```
## Next steps
- [Functions and recursion](/documentation/language/functions-and-recursion/) - destructure function arguments
- [Data structures](/documentation/language/data-structures/) - the collections you destructure
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Error handling
> Source: https://phel-lang.org/documentation/language/error-handling/
How Phel signals and recovers from failures: `throw` to raise, `try`/`catch`/`finally` to handle, and `ex-info` to carry structured data with an error. Phel uses PHP's exception machinery, so any PHP `Throwable` works here.
> This page is the canonical guide to handling errors in your code. Chasing a specific `[PHEL...]` compiler error code instead? See the [Error Reference](/documentation/reference/errors/).
## Throwing
```phel
(throw expr)
```
`throw` evaluates _expr_ and throws it. The value must implement PHP's `Throwable` (every PHP exception does).
```phel
(throw (php/new \Exception "Something went wrong"))
;; Shorthand for constructing a class: (Class. args)
(throw (Exception. "Something went wrong"))
```
## Try, catch, finally
```phel
(try expr* catch-clause* finally-clause?)
```
`try` evaluates its body. If nothing throws, it returns the last value. If a `catch` clause matches the thrown type, it returns that clause's value. A `finally` clause always runs last, for cleanup.
```phel
(try
(throw (Exception. "boom"))
(catch \Exception e "recovered")) ; => "recovered"
(try
(+ 1 1)
(finally (print "cleanup"))) ; => 2, and prints "cleanup"
(try
(throw (Exception. "boom"))
(catch \Exception e "recovered")
(finally (print "cleanup"))) ; => "recovered", and prints "cleanup"
```
A `catch` clause names the exception type and a symbol bound to the caught value. List several clauses to handle types differently; the first matching one wins.
```phel
(try
(throw (php/new \InvalidArgumentException "bad input"))
(catch \InvalidArgumentException e (str "arg error: " (php/-> e (getMessage))))
(catch \Exception e "other error"))
; => "arg error: bad input"
```
## Catching PHP exceptions
Anything PHP can throw, you can catch. Reference the PHP class with a leading backslash (`\Exception`, `\RuntimeException`, `\TypeError`). Read its details with PHP method calls via `php/->`.
```phel
(try
(throw (php/new \RuntimeException "disk full"))
(catch \Exception e
(php/-> e (getMessage)))) ; => "disk full"
```
`php/->` is the PHP method-call operator: `(php/-> e (getMessage))` is the same as `$e->getMessage()` in PHP. Use it to reach `getCode`, `getFile`, `getLine`, and friends.
Same exceptions, different shape:
```php
// PHP
try {
throw new \RuntimeException("disk full");
} catch (\Exception $e) {
echo $e->getMessage();
}
```
```phel
;; Phel
(try
(throw (php/new \RuntimeException "disk full"))
(catch \Exception e (php/-> e (getMessage))))
```
## Structured errors with `ex-info`
A plain message is often not enough. `ex-info` builds an exception that carries a data map (and an optional cause), so handlers can branch on machine-readable context instead of parsing strings.
```phel
(ex-info message data)
(ex-info message data cause)
```
```phel
(throw (ex-info "User not found" {:user-id 42 :status 404}))
```
Read the parts back with `ex-message`, `ex-data`, and `ex-cause`:
```phel
(def err (ex-info "Validation failed" {:field :email :reason "invalid format"}))
(ex-message err) ; => "Validation failed"
(ex-data err) ; => {:field :email :reason "invalid format"}
(ex-cause err) ; => nil (no cause provided)
```
### Branching on data
```phel
(try
(throw (ex-info "User not found" {:status 404}))
(catch \Exception e
(case (:status (ex-data e))
404 "not found"
403 "forbidden"
"unknown error"))) ; => "not found"
```
### Chaining a cause
Pass the original exception as the third argument to keep the failure trail. Read it back with `ex-cause`.
```phel
(try
(try
(throw (php/new \Exception "io fail"))
(catch \Exception e
(throw (ex-info "save failed" {:op :save} e))))
(catch \Exception e
(str (ex-message e) " <- " (ex-message (ex-cause e)))))
; => "save failed <- io fail"
```
`ex-info`, `ex-data`, `ex-message`, and `ex-cause` work as in Clojure. The underlying object is a PHP exception, so `catch \Exception` also catches `ex-info` values.
## When to throw vs return nil
Throwing is for genuinely exceptional situations. For ordinary "no result" cases, returning `nil` is often cleaner and lets callers use `if-let`, `when-let`, or a default.
- **Return `nil`** when absence is expected and the caller can handle it: a lookup miss, an empty parse, an optional field.
- **Throw** when continuing would be a bug or the caller cannot reasonably proceed: invalid arguments, broken invariants, failed I/O.
```phel
;; Expected miss: return nil, let the caller decide
(defn find-user [users id]
(get users id)) ; nil when not present
;; Real failure: throw with context
(defn charge-card [amount]
(when (<= amount 0)
(throw (ex-info "Invalid charge amount" {:amount amount})))
amount)
(find-user {} 42) ; => nil
(charge-card 10) ; => 10
```
## Next steps
- [Control flow](/documentation/language/control-flow/) - `if`, `cond`, and `case` for handling results
- [Basic types](/documentation/language/basic-types/) - why only `false` and `nil` are falsy
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Namespaces
> Source: https://phel-lang.org/documentation/language/namespaces/
How Phel organizes code across files: every file declares a namespace with `ns`, then pulls in Phel modules and PHP classes through requires.
## Namespace (ns)
Every Phel file needs a namespace. Names start with a letter, then letters/numbers/dashes. Parts separated by `.` (canonical) or `\` (legacy, still parses). Last part must match filename.
```phel
(ns name imports*)
```
Sets the namespace and registers imports. `:use` for PHP classes, `:require` for Phel modules, `:require-file` for PHP files.
```phel
(ns my.custom.module
(:require-file "vendor/autoload.php")
(:require my.phel.module)
(:use Some.Php.Class))
```
Also sets `*ns*` to the namespace.
Similar to PHP namespaces, with differences:
```php
// PHP
namespace My\Custom\Module;
use Some\Php\Class;
use My\Phel\Module as Utilities;
// Phel
(ns my.custom.module
(:use Some.Php.Class)
(:require my.phel.module :as utilities))
```
**Differences:**
- `.` separator for Phel namespaces (PHP class FQNs in `:use` use `.`)
- `:require` for Phel modules, `:use` for PHP classes
- Access via `/`, not `::`
Like Clojure: `.` namespace separator. PHP class FQNs in `:use` use `.`.
- `:use` is for PHP classes
- `:require` works as in Clojure
### Import a Phel module
Import with `:require`, then access as `module/name`. Namespaces resolve from `src/` (override with [configuration](/documentation/configuration/)).
Module `util` in namespace `hello-world`:
```phel
(ns hello-world.util)
(def my-name "Phel")
(defn greet [name]
(print (str "Hello, " name)))
```
Module `main` imports `util`:
```phel
(ns hello-world.main
(:require hello-world.util))
(util/greet util/my-name)
```
Use aliases to avoid collisions:
```phel
(ns hello-world.main
(:require hello-world.util :as utilities))
```
On collision, use a fully-qualified name to reach the original. A locally defined `get` shadows `phel.core/get` by its short name, but the full `phel.core/get` still works:
```phel
(ns hello-world.http-client)
(defn get [uri]
{:status 200 :body "Hello World" :headers {}})
(phel.core/get (get "https://example.com") :status) ; Evaluates to 200
```
`:refer` brings specific symbols into the current namespace so you can call them unqualified:
```phel
(ns hello-world.main
(:require hello-world.util :refer [greet]))
(greet util/my-name)
```
This works for standard-library modules too:
```phel
(ns my.app
(:require phel.string :refer [join split]))
(join ", " ["a" "b" "c"]) ; => "a, b, c"
(split "a,b,c" #",") ; => ["a" "b" "c"]
```
`:refer` and `:as` combine in any order.
### Import a PHP class
`:use` imports PHP classes:
```phel
(ns my.custom.module
(:use Some.Php.ClassName))
```
Reference by name:
```phel
(ClassName.) ; preferred shorthand
(php/new ClassName) ; also valid
```
Aliases avoid collisions:
```phel
(ns my.custom.module
(:use Some.Php.ClassName :as BetterClassName))
```
Importing is preferred, but optional. Use full namespace inline if needed:
```phel
(php/new Some.Php.ClassName) ; or: (Some.Php.ClassName.)
```
## Require PHP files
Load external PHP files via `:require-file` (calls `require_once`). Example for Composer autoload:
```phel
(ns hello-world.main
(:require-file "vendor/autoload.php"))
```
`(php/require_once "vendor/autoload.php")` works elsewhere, but for autoload it runs too late since Phel's core needs the autoloader. Use `:require-file`.
## Namespaced keywords
Plain keywords collide when sharing data. Namespaced keywords solve this.
Fully qualified: namespace, `/`, keyword name.
```phel
:my.namespace/foo ; absolute namespaced keyword
```
`::` shortcut binds current namespace:
```phel
(ns bar)
::foo ; => :bar/foo
```
`ns` aliases also work:
```phel
(ns foobar
(:require abc.xyz :as bar))
::bar/foo ; evaluates to :abc.xyz/foo
```
## Best practices
- **One namespace per file.** The last part of the namespace must match the filename, so a file maps to exactly one `ns`.
- **Dashes map to PHP.** Use `kebab-case` namespace names (`my.user-service`); Phel translates dashes to a valid PHP namespace when compiling.
- **Prefer `:as` over heavy `:refer`.** A short alias (`(:require phel.string :as str)`) keeps call sites clear about where a function comes from. Reserve `:refer` for a few frequently used names. Over-referring hides origins and invites collisions.
## Next steps
- [Interfaces](/documentation/language/interfaces/) - share behavior across types within a namespace
- [Configuration](/documentation/configuration/) - set the source paths namespaces resolve from
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# Macros
> Source: https://phel-lang.org/documentation/language/macros/
Macros are compile-time callables. They receive unevaluated code as data, transform it, and return new code for the compiler to process. This lets you add new syntax that functions cannot express.
## Why macros
In PHP, you cannot add new language constructs. Want `unless` (the opposite of `if`)? You are stuck with a function. Functions evaluate all arguments before the call, which breaks short-circuit logic and makes them second-class compared to `if`:
```php
// PHP: forced to use closures to avoid premature evaluation
function unless(bool $cond, callable $then, callable $else): mixed {
return $cond ? $else() : $then();
}
```
In Phel, a macro receives the raw code unevaluated, rewrites it, and the result compiles normally:
```phel
(defmacro unless [test then else]
`(if (not ~test) ~then ~else))
(unless false "yes" "no") ; => "yes"
;; Expands to: (if (not false) "yes" "no")
;; Only "yes" is ever evaluated. Behaves identically to a built-in if.
```
This works because **Phel code is data**. The call `(unless false "yes" "no")` is a plain Phel list, the same persistent list you work with everywhere. Macros manipulate that list at compile time using ordinary Phel functions.
`defn`, `when`, `and`, `or`, `->`, `->>` are all macros in Phel's standard library. They are not special compiler syntax. They are Phel code that rewrites other Phel code.
`defn` itself expands to `def` + `fn`:
```phel
(defn add [a b] (+ a b))
;; expands to:
(def add (fn [a b] (+ a b)))
```
PHP has no macro system. The common alternatives each have significant limitations:
- `eval()` runs at runtime, has security implications, and cannot be type-checked or linted
- Code generation produces files on disk, requires a build step, and the output is opaque
- Attributes are metadata only. They cannot transform the code they annotate.
Phel macros run at compile time inside the compiler pipeline, produce normal Phel AST nodes, and are fully inspectable with `macroexpand`.
## Quote
`quote` returns its argument unevaluated. Single-quote prefix is shorthand for `(quote form)`.
```phel
(quote my-sym) ; => my-sym
'my-sym ; same
```
Quote distinguishes code from data, making macros possible. Literals (numbers, strings) evaluate to themselves.
```phel
(quote 1) ; Evaluates to 1
(quote hi) ; Evaluates to the symbol hi
(quote quote) ; Evaluates to the symbol quote
'(1 2 3) ; Evaluates to the list (1 2 3)
'(print 1 2 3) ; Evaluates to the list (print 1 2 3). Nothing is printed.
```
## Define a macro
```phel
(defmacro name docstring? attributes? [params*] expr*)
```
`defmacro` creates a macro. Same params as `defn`.
With `quote` and `defmacro`, define a custom `defn` called `mydefn`:
```phel
(defmacro mydefn [name args & body]
(list 'def name (apply list 'fn args body)))
```
Simple, doesn't cover all `defn` features, but shows the basics.
## Quasiquote
`quasiquote` improves macro readability. Inverts quoting: marks what *should* evaluate, leaves the rest unevaluated. Shorthand: `` ` `` (quasiquote), `~` (unquote), `~@` (unquote-splicing).
`mydefn` with quasiquote:
```phel
(defmacro mydefn [name args & body]
`(def ~name (fn ~args ~@body)))
```
Same quasiquote/unquote/splicing tokens as Clojure.
## Expanding macros
To see what a macro produces, expand it without running it. `macroexpand-1` does a single expansion step; `macroexpand` keeps expanding until the top form is no longer a macro call. Quote the form so it stays code.
Expanding the `unless` macro from [Why macros](#why-macros):
```phel
(macroexpand-1 '(unless false "yes" "no"))
; => (if (phel.core/not false) "yes" "no")
(macroexpand-1 '(when true 1 2))
; => (if true (do 1 2))
```
Quasiquote fully qualifies referenced symbols (`not` becomes `phel.core/not`), which is what keeps macros from breaking when the caller has shadowed a name. This is your main debugging tool: if a macro misbehaves, expand it and read the generated code.
## Hygiene and `gensym`
A macro that introduces its own local bindings can accidentally capture (shadow) a name from the caller. To avoid this, generate a unique symbol with `gensym`:
```phel
(gensym) ; => __phel_1 (a fresh, unique name on every call)
(gensym) ; => __phel_2
```
Inside a quasiquote, the `name#` suffix auto-generates a `gensym` for you, so the same `name#` refers to one fresh symbol throughout the template:
```phel
(defmacro my-or [a b]
`(let [tmp# ~a]
(if tmp# tmp# ~b)))
(my-or false 42) ; => 42
(macroexpand-1 '(my-or false 42))
; => (let [tmp__1 false] (if tmp__1 tmp__1 42))
```
The expanded `tmp__1` is unique per expansion, so it cannot clash with a `tmp` the caller already has. Reach for `gensym` (or `name#`) whenever a macro binds a local the user did not write.
## When to write a macro
Most of the time you do not need one. **Prefer a function.** Functions are easier to read, test, compose, and pass around. Reach for a macro only when a function genuinely cannot do the job:
- **New syntax or binding forms** the language does not provide.
- **Control flow** that must skip or reorder evaluation of its arguments (a function evaluates all its arguments first).
- **Compile-time work**, where you want code generated or checked before the program runs.
If the same result is achievable by passing values or functions, write a function.
## Next steps
- [Functions and recursion](/documentation/language/functions-and-recursion/) - the default tool; prefer it over macros
- [Basic types](/documentation/language/basic-types/) - quote, lists, and symbols that macros manipulate
- [Cheat sheet](/documentation/reference/cheat-sheet/) - keep it open while coding
---
# PHP Interop
> Source: https://phel-lang.org/documentation/php-interop/
## Globals and constants
Access PHP superglobals with `php/` prefix and `get`:
```phel
(get php/$_SERVER "key") ; $_SERVER['key']
(get php/$GLOBALS "argv") ; $GLOBALS['argv']
```
PHP [`define`](https://www.php.net/manual/en/function.define.php) constants accessed via `php/CONSTANT_NAME`:
```phel
(php/define "MY_SETTING" "My value") ; Calls PHP define('MY_SETTING', 'My value');
php/MY_SETTING ; => "My value"
```
The `php/` prefix gives you direct access to PHP's global scope:
```php
// PHP
$_SERVER['key']
$GLOBALS['argv']
MY_SETTING
// Phel
(get php/$_SERVER "key")
(get php/$GLOBALS "argv")
php/MY_SETTING
```
**Note:** Use Phel's immutable data structures when possible. Only use PHP arrays when you need to interop with PHP libraries that expect them.
## Calling PHP functions
Add `php/` prefix to any PHP function name:
```phel
(php/strlen "test") ; => 4
(php/date "l") ; => "Monday" (or whatever the current day is)
```
Any PHP function can be called by adding the `php/` prefix:
```php
// PHP
strlen("test");
date("l");
array_map($fn, $array);
// Phel
(php/strlen "test")
(php/date "l")
(php/array_map fn array)
```
However, Phel provides functional equivalents for many operations. For example, use `(count "test")` instead of `(php/strlen "test")` when working with Phel data structures.
Namespaced PHP functions use full path after `php/`. Three equivalent forms accepted (last two are backslash-free):
```phel
(php/Foo\Bar\baz) ; classic backslash form
(php/Foo.Bar/baz) ; dot-separated, slash before fn name
(php/Foo.Bar.baz) ; fully dot-separated
(php/Amp.trapSignal [(php/:: SIGINT) (php/:: SIGTERM)])
```
Capture into a Phel alias:
```phel
(def trap-signal php/\Amp.trapSignal)
(trap-signal [2 15])
```
## Interop shorthands
Terse forms that expand to verbose `php/*`. Use whichever reads better.
| Shorthand | Expands to |
|---------------------------|------------------------------------|
| `(ClassName. args)` | `(php/new ClassName args)` |
| `(new ClassName args)` | `(php/new ClassName args)` |
| `(.method obj args)` | `(php/-> obj (method args))` |
| `(.-field obj)` | `(php/-> obj field)` |
| `(ClassName/method args)` | `(php/:: ClassName (method args))` |
| `ClassName/MEMBER` | `(php/:: ClassName MEMBER)` |
```phel
(ns my.module
(:use DateTimeImmutable DateInterval))
(DateTimeImmutable. "2026-04-20") ; constructor (preferred)
(.format (DateTimeImmutable.) "Y-m-d") ; instance method
(.-s (DateInterval. "PT30S")) ; property
(DateTimeImmutable/createFromFormat "Y-m-d" "2026-04-20") ; static method
DateTimeImmutable/ATOM ; static constant
```
## Class instantiation
Three equivalent forms - prefer `ClassName.` for imported classes:
```phel
(ns my.module
(:use DateTime DateTimeImmutable))
(DateTime.) ; => DateTime instance (ClassName. shorthand)
(DateTime. "now") ; => DateTime instance with arg
(new DateTime) ; also valid
(php/new DateTime) ; also valid
(php/new "\\DateTimeImmutable") ; instantiate from string (dynamic)
```
```php
// PHP
new DateTime();
new DateTime("now");
new \DateTimeImmutable();
// Phel - preferred shorthand
(DateTime.)
(DateTime. "now")
(DateTimeImmutable.)
```
Import classes with `:use` to use the short `ClassName.` form without repeating the namespace.
## Method and property call
```phel
(php/-> object (methodname expr*))
(php/-> object property)
```
Calls method or accesses property. Both `methodname` and `property` must be symbols, not evaluated values.
Chain multiple in one `php/->`. Each element evaluates on result of previous, enabling fluent chains or nested property access.
```phel
(ns my.module
(:use DateInterval)
(:use DateTimeImmutable)
(:use stdClass))
(def di (DateInterval. "PT30S"))
(.format di "%s seconds") ; => "30 seconds" (.method shorthand)
(php/-> di (format "%s seconds")) ; same, verbose form
(.-s di) ; => 30 (.-prop shorthand)
;; Chain multiple calls:
;; (new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d")
(-> (DateTimeImmutable. "2024-03-10")
(.modify "+1 day")
(.format "Y-m-d"))
;; php/-> also works and is required for chains mixing methods and properties:
(php/-> user profile (getDisplayName))
;; Nested property access:
(def address (stdClass.))
(def user (stdClass.))
(php/oset (php/-> address city) "Berlin")
(php/oset (php/-> user address) address)
(php/-> user address city) ; => "Berlin"
```
The `php/->` operator is similar to PHP's `->` but allows chaining in a more functional style:
```php
// PHP
$di->format("%s seconds");
$di->s;
(new DateTimeImmutable("2024-03-10"))->modify("+1 day")->format("Y-m-d");
$user->profile->getDisplayName();
// Phel - shorthand forms
(.format di "%s seconds")
(.-s di)
(-> (DateTimeImmutable. "2024-03-10") (.modify "+1 day") (.format "Y-m-d"))
(php/-> user profile (getDisplayName)) ; mixed chains need php/->
```
Method calls: `(.method obj args)` shorthand or `(php/-> obj (method args))`. Property access: `(.-prop obj)` or `(php/-> obj prop)`. Mixed chains (method + property in one expression) use `php/->` directly.
The `php/->` operator is inspired by Clojure's thread-first macro `->`, but specifically designed for PHP object method chaining.
## Static method and property
```phel
(php/:: class (methodname expr*))
(php/:: class property)
```
Same as above, but static.
```phel
(ns my.module
(:use DateTimeImmutable))
DateTimeImmutable/ATOM ; => "Y-m-d\TH:i:sP" (shorthand)
(php/:: DateTimeImmutable ATOM) ; verbose form
(DateTimeImmutable/createFromFormat "Y-m-d" "2020-03-22") ; shorthand
(php/:: DateTimeImmutable (createFromFormat "Y-m-d" "2020-03-22")) ; verbose
```
The `php/::` operator is equivalent to PHP's `::` for static method and property access:
```php
// PHP
DateTimeImmutable::ATOM;
DateTimeImmutable::createFromFormat("Y-m-d", "2020-03-22");
// Phel - shorthand forms
DateTimeImmutable/ATOM
(DateTimeImmutable/createFromFormat "Y-m-d" "2020-03-22")
```
## Named arguments
PHP 8 named arguments are passed after a `:&` marker as `:key value` pairs. Works in `php/new`, `php/->`, and `php/::`. Keyword keys map to the PHP parameter names; order is then irrelevant.
```phel
(let [dt (php/:: \DateTime
(createFromFormat :& :format "Y-m-d" :datetime "2026-06-06"))]
(php/-> dt (format "Y-m-d"))) ; => "2026-06-06"
```
```php
// PHP
\DateTime::createFromFormat(format: "Y-m-d", datetime: "2026-06-06");
new \App\Mailer(host: "smtp", port: 587);
```
```phel
;; Phel
(php/:: \DateTime (createFromFormat :& :format "Y-m-d" :datetime "2026-06-06"))
(php/new \App\Mailer :& :host "smtp" :port 587)
```
## By-reference arguments
Some PHP functions write through a `&$ref` parameter (`preg_match`, `sort`, ...). Wrap a **local** binding in `php/ref` to pass it by reference; the local must be `let`-bound (a top-level `def` is not a PHP variable).
```phel
(let [subject "order-42"
matches (php/array)]
(php/preg_match "/(\d+)/" subject (php/ref matches))
(php/aget matches 1)) ; => "42"
```
`php/ref` also works inside `php/->` / `php/::` calls.
## Set object properties
```phel
(php/oset (php/-> object property) value)
(php/oset (php/:: class property) value)
```
Set value on class/object property.
```phel
(def x (stdclass.))
(php/oset (php/-> x name) "foo")
```
`php/oset` is the Phel equivalent of PHP's property assignment:
```php
// PHP
$x = new stdClass();
$x->name = "foo";
// Phel
(def x (stdclass.))
(php/oset (php/-> x name) "foo")
```
**Note:** This mutates the PHP object. When possible, use Phel's immutable data structures instead.
## Type conversions
Phel values and PHP values cross the boundary automatically for scalars (int, float, string, bool, nil). Collections differ: Phel uses immutable vectors/maps, PHP uses arrays. Convert explicitly when a library needs one or the other.
| Function | Direction | Example | Result |
|---|---|---|---|
| `to-php-array` | Phel vector/map to PHP array | `(to-php-array [1 2 3])` | `` |
| `phel->php` | deep Phel to PHP (nested) | `(phel->php {:a 1 :b 2})` | `` |
| `php->phel` | deep PHP to Phel (nested) | `(php->phel (php/array 1 2 3))` | `[1 2 3]` |
| `php-array-to-map` | PHP array to Phel map | `(php-array-to-map #php {"a" 1 "b" 2})` | `{"a" 1, "b" 2}` |
```phel
(to-php-array [1 2 3]) ; =>
(php->phel (php/array 1 2 3)) ; => [1 2 3]
(php-array-to-map #php {"a" 1}) ; => {"a" 1}
(phel->php {:a 1}) ; =>
```
Use `#php [...]` and `#php {...}` reader macros to write PHP array literals directly.
## Checking types
`php/instanceof` tests an object against a PHP class or interface:
```phel
(php/instanceof (php/new \DateTime) \DateTimeInterface) ; => true
```
For Phel's own values use the core predicates (`int?`, `string?`, `map?`, `vector?`, ...).
## PHP functions as values
A `php/`-prefixed function is a first-class value. Bind it, pass it, or spread arguments into it with `apply`:
```phel
(let [upcase php/strtoupper]
(map upcase ["a" "b"])) ; => @["A" "B"]
(apply php/max [3 7 2]) ; => 7
```
Capture a namespaced PHP function into a Phel alias the same way:
```phel
(def trap-signal php/\Amp.trapSignal)
(trap-signal [2 15])
```
## Magic methods on structs
A `defstruct` is a real PHP class, so it can expose magic methods (`__invoke`, `__toString`, `__get`, ...) through an inline `:php` block. See [Structs](/documentation/language/data-structures/#structs) for the full form.
```phel
(defstruct money [cents]
:php
(__toString [this] (str "$" (/ (get this :cents) 100))))
(php/strval (money 500)) ; => "$5"
```
## Get PHP array value
```phel
(php/aget arr index)
```
Equivalent: `arr[index] ?? null`.
```phel
(php/aget ["a" "b" "c"] 0) ; Evaluates to "a"
(php/aget (php/array "a" "b" "c") 1) ; Evaluates to "b"
(php/aget (php/array "a" "b" "c") 5) ; Evaluates to nil
```
`php/aget` safely accesses PHP array elements:
```php
// PHP
$arr[0] ?? null;
$arr[1] ?? null;
$arr[5] ?? null; // Returns null
// Phel
(php/aget arr 0)
(php/aget arr 1)
(php/aget arr 5) ; Returns nil
```
**Important distinction:**
- Use `php/aget` for **PHP arrays** (mutable)
- Use `get` for **Phel data structures** (immutable vectors, maps)
## Get nested PHP array value
```phel
(php/aget-in arr path)
```
Resolves nested values via a sequence of keys/indexes. `path` is a sequential collection (e.g. vector). Missing step returns `nil`.
```phel
(def users
#php {"users"
#php {0 #php {"name" "Alice"}
1 #php {"name" "Bob"}}})
(php/aget-in users ["users" 1 "name"]) ; Evaluates to "Bob"
(php/aget-in
#php {"meta" #php {"status" "ok"}}
["meta" "status"]) ; Evaluates to "ok"
(php/aget-in
#php {"meta" #php {"status" "ok"}}
["meta" "missing"]) ; Evaluates to nil
```
`php/aget-in` provides safe nested array access:
```php
// PHP - manual nested access with null coalescing
$users['users'][1]['name'] ?? null;
$data['meta']['status'] ?? null;
$data['meta']['missing'] ?? null;
// Phel - clean path-based access
(php/aget-in users ["users" 1 "name"])
(php/aget-in data ["meta" "status"])
(php/aget-in data ["meta" "missing"]) ; Returns nil safely
```
This is similar to Phel's `get-in` for immutable data structures, but specifically for PHP arrays.
## Set PHP array value
```phel
(php/aset arr index value)
```
Equivalent: `arr[index] = value`.
`php/aset` mutates a PHP array in place:
```php
// PHP
$arr[0] = "value";
// Phel
(php/aset arr 0 "value")
```
**Important:** This mutates the array. For immutable operations, use Phel's `assoc` on Phel data structures instead.
## Set nested PHP array value
```phel
(php/aset-in arr path value)
```
Creates or updates nested entries. Missing intermediate arrays are created.
```phel
(def data (php/array))
(php/aset-in data ["user" "profile" "name"] "Charlie")
(php/aget-in data ["user" "profile" "name"]) ; Evaluates to "Charlie"
;; Equivalent to $data['user']['profile']['name'] = 'Charlie';
```
`php/aset-in` creates nested structures automatically:
```php
// PHP - manual nested array creation
$data = [];
$data['user']['profile']['name'] = 'Charlie';
// Phel - automatic path creation
(def data (php/array))
(php/aset-in data ["user" "profile" "name"] "Charlie")
```
This is the mutable counterpart to Phel's `assoc-in` for immutable data structures.
## Append PHP array value
```phel
(php/apush arr value)
```
Equivalent: `arr[] = value`.
`php/apush` appends to a PHP array:
```php
// PHP
$arr[] = "new value";
// Phel
(php/apush arr "new value")
```
For immutable operations, use `conj` on Phel vectors instead.
## Unset PHP array value
```phel
(php/aunset arr index)
```
Equivalent: `unset(arr[index])`.
`php/aunset` removes an element from a PHP array:
```php
// PHP
unset($arr[0]);
// Phel
(php/aunset arr 0)
```
For immutable operations, use `dissoc` on Phel maps instead.
## Unset nested PHP array value
```phel
(php/aunset-in arr path)
```
Removes nested entry. Parent arrays remain untouched even if empty after.
```phel
(def data #php {"user" #php {"profile" #php {"name" "Dora"}}})
(php/aunset-in data ["user" "profile" "name"])
(php/aget-in data ["user" "profile" "name"]) ; Evaluates to nil
;; Equivalent to unset($data['user']['profile']['name']);
```
`php/aunset-in` removes nested array elements:
```php
// PHP
unset($data['user']['profile']['name']);
// Phel
(php/aunset-in data ["user" "profile" "name"])
```
Parent arrays remain intact even if they become empty after the unset.
## `__DIR__`, `__FILE__`, `*file*`
PHP magic constants `__DIR__` and `__FILE__` work but expand at PHP compile, pointing to the generated PHP file under `.phel/cache`.
For the original Phel source path, use `*file*` (absolute path of current Phel file). Combine with `php/dirname` for the source dir.
```phel
(println __DIR__) ; Directory name of the generated PHP file
(println __FILE__) ; Filename of the generated PHP file
(println (php/dirname *file*)) ; Directory of the original Phel file
(println *file*) ; Absolute path of the original file
```
**Important distinction:**
```php
// PHP magic constants
__DIR__ // Points to .phel/cache directory (generated PHP)
__FILE__ // Points to cached .php file
// Phel special var
*file* // Points to your actual .phel source file
```
Use `*file*` when you need to reference the original Phel source location, such as for loading resources relative to your source code.
## Map to typed object and back
`hydrate` and `bean` bridge a Phel map and a typed PHP object both ways: `hydrate` rebuilds an instance from a map (skipping the constructor, like an ORM rehydrating an entity), and `bean` reads an object's public properties back into a map with keyword keys.
```phel
;; class App\Point { public int $x; public int $y; }
(def p (hydrate "App\\Point" {:x 1 :y 2})) ; => App\Point instance
(bean p) ; => {:x 1 :y 2}
```
To read PHP 8 attributes and bridge native enums, see `phel.reflect`
(`class-attributes`, `enum->keyword`, ...) in the
[API reference](/documentation/reference/api/reflect).
## Native enums and exceptions
`defenum` compiles to a native PHP backed enum (e.g. for Doctrine/Symfony columns), plus a `Name?` predicate. The enum is a real PHP type: consume it from PHP, reference it by full name (`\my\ns\Status`), or bridge cases to keywords with `phel.reflect` (see [Reflection](#reflection-attributes-and-enums)).
```phel
(defenum Status :active "active" :inactive "inactive")
;; emits: enum Status: string { case active = "active"; case inactive = "inactive"; }
```
`defexception` defines an exception extending a chosen parent, so framework `catch` blocks match it by type:
```phel
(defexception NotFound \RuntimeException)
(try
(throw (NotFound "missing"))
(catch \RuntimeException e (php/-> e (getMessage)))) ; => "missing"
```
## Reflection: attributes and enums
`phel.reflect` reads PHP 8 attributes and bridges native enums (including `defenum` output) to keywords and back. Pass classes/enums by full name.
```phel
(ns my-app
(:require phel\reflect :as reflect))
```
Attributes come back as `{:name :args}` maps:
| Function | Reads |
|---|---|
| `class-attributes` | attributes on a class |
| `method-attributes` | attributes on a method |
| `property-attributes` | attributes on a property |
```phel
;; #[Tag('x')] class Thing {}
(reflect/class-attributes \Demo\Thing)
; => [{:name "Demo\\Tag" :args {0 "x"}}]
```
Enum bridge:
| Function | Does |
|---|---|
| `enum-values` | all cases as keywords |
| `enum->keyword` | one case to its keyword |
| `keyword->enum` | keyword back to the case |
```phel
;; enum Suit: string { case Hearts = 'H'; case Spades = 'S'; }
(reflect/enum-values \Demo\Suit) ; => [:Hearts :Spades]
(reflect/enum->keyword (php/:: \Demo\Suit Hearts)) ; => :Hearts
(reflect/keyword->enum \Demo\Suit :Spades) ; => Suit::Spades
```
## Catching PHP exceptions
PHP functions and methods throw native exceptions, and they cross the interop boundary unchanged. Catch them with `try`/`catch`, matching on the PHP class name. Catch `\Throwable` to handle anything.
```phel
(try
(php/intdiv 1 0)
(catch \DivisionByZeroError e
(php/-> e (getMessage))))
; => "Division by zero"
```
The `.method` shorthand and a `finally` clause work too:
```phel
(try
(risky-php-call)
(catch \Throwable e
(.getMessage e))
(finally
(cleanup)))
```
For Phel's own exceptions, `ex-info`, and re-throwing, see [Error Handling](/documentation/language/error-handling/).
## Calling Phel from PHP
Useful for integrating Phel into existing PHP apps. Load the Phel namespace after `autoload.php`.
Example: [using-exported-phel-function.php](https://github.com/phel-lang/cli-skeleton/blob/main/example/using-exported-phel-function.php)
```php
adder(1, 2, 3);
echo 'Result = ' . $result . PHP_EOL;
```
Two ways: manually, or via the `export` command.
### Manually
`PhelCallerTrait` calls any Phel function from a PHP class. Inject the trait, call `callPhel`.
```php
callPhel(
'my.phel.namespace',
'phel-function-name',
...$arguments
);
}
}
```
### Using the `export` command
`phel export` generates a wrapper class for all Phel functions marked *export*.
Set the `withExportFromDirectories`, `withExportNamespacePrefix`, and `withExportTargetDirectory` options in `phel-config.php` first: see [Configuration](/documentation/configuration/#full-reference).
Mark a function exported with metadata:
```phel
(defn my-function
{:export true}
[a b]
(+ a b))
```
`phel export` then generates a wrapper class in the target dir (here `src/PhelGenerated`). Use it from PHP to call Phel functions.
### Typed and annotated output
When the generated PHP must satisfy a framework's type expectations, opt-in metadata (`^{:tag T}`, `^{:php/attr [...]}`, `^{:php/doc "..."}`, `^:php/readonly`, and more) enriches it; untagged forms are unchanged. For the full metadata table and a Doctrine-entity `defstruct` example, see [Typed PHP from Phel definitions](/documentation/web/framework-integration/#typed-php-from-phel-definitions).
## Next steps
- [Error Handling](/documentation/language/error-handling/): `try`, `catch`, `finally`, `ex-info`.
- [Configuration](/documentation/configuration/): `withExport*` options for `phel export`.
- [PHP API reference](/documentation/reference/api/php): every `php/*` builtin.
- [Rosetta Stone](/documentation/guides/rosetta-stone/): PHP and Phel side by side, interop included.
---
# CLI Commands
> Source: https://phel-lang.org/documentation/tooling/cli-commands/
Every task you run through Phel goes through one CLI. This page lists the built-in commands with a working example for each.
```bash
# Overview of all commands
vendor/bin/phel list
```
## Initialize a new project
Scaffold a new Phel project:
```bash
vendor/bin/phel init
# Usage:
# init [options] [--] []
#
# Arguments:
# project-name The project/namespace name (default: "app")
#
# Options:
# --nested Use nested layout (src/phel/, tests/phel/)
# -m, --minimal Use root layout (single main.phel at project root)
# --force Overwrite existing files
# --dry-run Show what would be created without writing anything
# --no-gitignore Skip generating .gitignore
# --no-tests Skip generating a test file
# -t, --template[=NAME] Scaffold from a bundled example; omit the value to list
# --list-templates List available project templates and exit
```
Defaults to **Flat** layout (`src/`, `tests/`). `--nested` for `src/phel/`. `--minimal` for a single root file.
```bash
# Flat layout (default)
vendor/bin/phel init my-app
# Nested layout
vendor/bin/phel init my-app --nested
# Preview what would be created
vendor/bin/phel init my-app --dry-run
```
Scaffold from a bundled, runnable example instead of a bare skeleton. The template's namespaces, `composer.json`, and entry points are renamed to your project name. Composes with `--dry-run` and `--force`.
```bash
# List the bundled templates
vendor/bin/phel init --list-templates
# http-json-api, todo-app, cli-wordcount
# Scaffold a project from a template
vendor/bin/phel init my-api --template=http-json-api
```
## Build the project
```bash
vendor/bin/phel build
# Usage:
# build [options]
#
# Options:
# --cache|--no-cache Enable cache
# --source-map|--no-source-map Enable source maps
# -O, --optimization-level=LEVEL Override configured level (0 = off, 2 = inline + tail-call rewrite)
# --report Print a build report (namespaces, sizes, time)
```
Compiles Phel to PHP, writing to the configured main path (entry point `out/index.php`). Run the resulting PHP directly. Skips recompilation, improving runtime.
```bash
# Build with optimizations on (inlining + self-recursive tail-call rewriting)
vendor/bin/phel build -O 2
# Print a build summary to spot bloat and verify CI builds
vendor/bin/phel build --report
```
`-O` overrides the level set via `withOptimizationLevel(...)` in `phel-config.php`. See [Performance](/documentation/performance/) for what each level does. `--report` prints namespace count, per-namespace compiled size, total size, the fresh/cached breakdown, and build time.
[Configuration](/documentation/configuration/) in `phel-config.php`:
```php
withMainPhelNamespace('your-ns.index')
->withMainPhpPath('out/index.php');
```
## Export definitions
Exports definitions with `{:export true}` metadata as PHP classes. Generates one class per namespace, one method per exported definition. Lets you call Phel functions from PHP.
```bash
vendor/bin/phel export
```
Configure the export dirs, namespace prefix, and target directory in `phel-config.php`; see [Configuration](/documentation/configuration/#full-reference).
## Format phel files
Formats files. Accepts relative or absolute paths.
```bash
vendor/bin/phel format # formats src and tests by default
vendor/bin/phel format src/foo.phel
vendor/bin/phel format --dry-run # report files that would change, exit non-zero if any
```
[Configuration](/documentation/configuration/) in `phel-config.php`:
```php
withFormatDirs(['src', 'tests']);
```
Indents definition and body forms (`defstruct`, `defprotocol`, `defmethod`, `reify`, `doseq`, `letfn`, ...) cljfmt-style, and collapses consecutive blank lines to one.
## Read-eval-print loop
Interactive prompt for quick tests and language exploration.
```bash
vendor/bin/phel repl
```
See [REPL](/documentation/tooling/repl).
## Run a script
Run a file or namespace:
```bash
vendor/bin/phel run
# Usage:
# run [options] [--] [...]
#
# Arguments:
# path The file path that you want to run.
# argv Optional arguments
#
# Options:
# -t, --with-time With time awareness
```
[Configuration](/documentation/configuration/) in `phel-config.php`:
```php
withSrcDirs(['src']);
```
See [Getting Started](/documentation/getting-started/).
## Test your Phel logic
Runs tests. No paths runs everything in `tests/`.
```bash
vendor/bin/phel test
# Usage:
# test [options] [--] [...]
#
# Arguments:
# paths The file paths that you want to test.
#
# Options:
# -f, --filter[=REGEX] Filter by test name regex. Repeatable.
# --fail-fast Stop on first failure or error.
# --include=TAG Only run tests tagged TAG. Repeatable.
# --exclude=TAG Skip tests tagged TAG. Repeatable.
# --ns=GLOB Only run namespaces matching GLOB. Repeatable.
# --reporter=NAME Reporter: default|testdox|dot|tap|junit-xml. Repeatable.
# --output=PATH Output path (for junit-xml).
# --testdox Shortcut for --reporter=testdox.
# --repeat=N Run each test N times (default 1).
# --seed=INT Seed used for randomized order.
# --random-order Run tests in random order (uses --seed if given).
# --parallel=N Run namespaces in subprocess workers: int, "auto" (capped at 8), or "max".
# --watch Re-run selected tests on every .phel / phel-config.php change.
# --last-failed Re-run only tests that failed on the previous run.
# --slowest=N Print the N slowest tests after the summary (0 disables).
# --stack-trace Print the full PHP stack trace for each errored test.
# --coverage[=FORMAT] Collect line coverage (text|clover) via pcov or xdebug.
# --coverage-output=PATH Write the coverage report to a file (use with --coverage=clover for CI).
```
See [Testing](/documentation/testing/) for what each flag does.
[Configuration](/documentation/configuration/) in `phel-config.php`:
```php
withTestDirs(['tests']);
```
## Evaluate an expression
Evaluate and print. Pass a literal expression, or `-` for stdin.
```bash
vendor/bin/phel eval '(+ 1 2 3)'
# => 6
echo '(map inc [1 2 3])' | vendor/bin/phel eval -
# => (2 3 4)
```
For reliable multi-line evaluation, pass a quoted heredoc to stdin:
```bash
vendor/bin/phel eval - <<'PHEL'
(ns app)
(println (+ 40 2))
PHEL
```
Prefer this pattern because:
- **No quoting issues:** Everything between `<<'PHEL'` and `PHEL` is treated as literal input.
- **Consistent pattern:** One approach works for all evaluations, from simple to complex.
- **Multi-line friendly:** Code keeps its natural, readable formatting.
- **Easy to extend:** Add more forms without changing the command syntax.
## Compile to PHP
Emit the PHP that Phel generates for a snippet, file, or stdin, without evaluating it. Handy for understanding the compiler or debugging interop.
```bash
vendor/bin/phel compile '(php/strlen "hello")'
# => strlen("hello");
vendor/bin/phel compile src/main.phel # compile a file
echo '(map inc [1 2 3])' | vendor/bin/phel compile -
# Usage:
# compile [options] [--] []
#
# Arguments:
# source Phel expression, path to a .phel file, or "-" for stdin
#
# Options:
# -t, --target Compilation target (currently only "php")
```
## Lint
Static analysis. Rules: unresolved-symbol, arity-mismatch, unused-binding, unused-require, unused-import, shadowed-binding, redundant-do, duplicate-key, invalid-destructuring, discouraged-var.
```bash
vendor/bin/phel lint
# Usage:
# lint [options] [--] [...]
#
# Options:
# --format=FORMAT human (default), json, github
# --config=PATH Path to phel-lint.phel
# --no-cache Disable linter cache
```
Configure rules in `phel-lint.phel` at the project root.
## Watch
Reloads changed namespaces in dependency order. Backends: inotify, fswatch, polling.
```bash
vendor/bin/phel watch
# Usage:
# watch [options] [--] [...]
#
# Arguments:
# paths Files or directories to watch (default: configured src dirs)
#
# Options:
# -b, --backend=BACKEND Watcher backend: auto, inotify, fswatch, polling (default: auto)
# --poll=MS Polling interval in ms, polling backend only (default: 500)
# --debounce=MS Debounce window in ms (default: 100)
```
From Phel code, use `phel.watch`:
```phel
(ns my-app
(:require phel.watch :refer [watch!]))
(watch! ["src/"])
```
## nREPL
Bencode-over-TCP nREPL server for editor inline eval.
```bash
vendor/bin/phel nrepl --port=7888 --host=127.0.0.1
```
See [Editor support](/documentation/tooling/editor-support/#nrepl-and-editor-integration) for supported ops and connecting your editor.
## LSP
LSP v3.17 over stdio.
```bash
vendor/bin/phel lsp
```
See [Editor support](/documentation/tooling/editor-support/#language-server-lsp) for supported features and PHP-interop-aware completion.
## Analyze and index
`phel analyze ` emits JSON diagnostics; `phel index ...` builds a symbol table for tooling.
```bash
vendor/bin/phel analyze src/main.phel
vendor/bin/phel index src --out=symbols.json
```
`phel api-daemon` serves the Api facade as JSON-RPC over stdio.
```bash
vendor/bin/phel api-daemon
```
## Agent install
Writes skill/recipe files for AI coding assistants: Claude Code, Cursor, Codex, Gemini, Copilot, Aider. Copies a per-platform skill file plus the shared `.agents/` docs tree. Re-install is idempotent; existing files are backed up to `.pre-phel.bak` unless `--force`.
```bash
vendor/bin/phel agent-install # pick platform interactively
vendor/bin/phel agent-install claude # single platform
vendor/bin/phel agent-install --all # every platform
vendor/bin/phel agent-install --auto # only platforms detected in project
vendor/bin/phel agent-install --uninstall # remove skill files, restore .pre-phel.bak
# --no-docs Skip the .agents/ docs tree (copied by default)
# --with-examples Also copy example projects into .agents/examples/
# --dry-run Show what would be written, change nothing
# --force Overwrite without .pre-phel.bak backups
```
## Profile
Per-function timings and compile-phase costs:
```bash
vendor/bin/phel profile path/to/file.phel
# Options:
# --format=FORMAT text (default), json
# --output=PATH Write report to PATH
```
## Inspect configuration
Print the effective configuration and where each part comes from. Useful when a `phel-config.php`, a `phel-config-local.php` override, or the `PHEL_DIR` env var is not taking effect as you expect.
```bash
vendor/bin/phel config
# Sources:
# - project root: /path/to/project
# - phel-config.php: not found, using auto-detected defaults
# - phel-config-local.php: not present
# - PHEL_DIR env: (unset)
#
# Effective config:
# { "src-dirs": ["src"], "test-dirs": ["tests"], ... }
# Machine-readable: just the effective config as JSON
vendor/bin/phel config --json
```
See [Configuration](/documentation/configuration/) for every setter.
## Clear caches
Clear namespace and compiled-code caches:
```bash
vendor/bin/phel cache:clear
```
Removes everything in the cache dir. Useful for stale caches or after upgrades.
Runtime state (cache, REPL history, error log) lives under `.phel/` by default. Override via `withPhelDir('...')` in `phel-config.php` or the `PHEL_DIR` env var.
## Next steps
- [REPL](/documentation/tooling/repl/) - the interactive loop behind `phel repl`
- [Editor support](/documentation/tooling/editor-support/) - connect your editor to `phel nrepl`
- [Configuration](/documentation/configuration/) - tune paths, cache, and export in `phel-config.php`
---
# REPL
> Source: https://phel-lang.org/documentation/tooling/repl/
## Interactive prompt
The REPL is your fastest feedback loop in Phel: type an expression, press Enter, see the result. Use it to explore the language, test functions as you write them, and debug live.
Start:
```bash
./vendor/bin/phel repl
```
Type any expression, press Enter:
```phel
Welcome to the Phel Repl
Type "exit" or press Ctrl-D to exit.
user:1> (* 6 7)
42
user:2> (str "Hello, " "world!")
"Hello, world!"
```
Multiline works: prompt switches to `....` until the expression is complete.
```phel
user:1> (defn greet [name]
....:2> (str "Hello, " name "!"))
user:3> (greet "Phel")
"Hello, Phel!"
```
`Ctrl-D` or `exit` to quit.
Prompt shows current namespace (defaults to `user`), tracks `(ns ...)` and `(in-ns ...)`. `def` returns a printable var ref (e.g. `#'user/my-var`).
## History variables
REPL tracks recent results and the last exception:
- `*1` last result
- `*2` previous
- `*3` two before
- `*e` last exception
```phel
user:1> (+ 1 2)
3
user:2> (* *1 10)
30
user:3> (/ 1 0)
; => exception
user:4> (.getMessage *e)
"Division by zero"
```
Eval errors render as a headline + optional hint + trace with internal frames hidden. Full PHP frames remain on `*e` for inspection via interop.
## Built-in helpers
### doc
Show docs for any function or macro in scope:
```phel
user:1> (doc all?)
(all? pred coll)
Returns true if predicate is true for every element in collection, false otherwise.
nil
user:2> (doc map)
(map f & colls)
...
```
Fastest way to check signatures without leaving the REPL.
### require
Import a Phel namespace. Same args as `:require` in `ns`:
```phel
user:1> (require phel.html :as h)
phel.html
user:2> (h/html [:span {:class "greeting"} "Hello"])
Hello
```
### dir
List public definitions in a namespace:
```phel
user:1> (dir "phel.string")
blank?
capitalize
ends-with?
escape
...
```
### apropos
Search symbols by name across loaded namespaces. Returns a sorted vector of fully qualified names:
```phel
user:1> (apropos "map")
@["phel.core/flat-map" "phel.core/hash-map" "phel.core/map" "phel.core/map-indexed" "phel.core/mapcat"]
```
### search-doc
Search docstrings. Prints each matching definition with its docs:
```phel
user:1> (search-doc "lazy")
--- phel.core/concat ---
(concat & xs)
Returns the concatenation of all xs ... Lazily evaluated, so xs can be lazy seqs.
...
```
### use
Alias a PHP class. Same as `:use` in `ns`:
```phel
user:1> (use DateTimeImmutable)
DateTimeImmutable
user:2> (.format (DateTimeImmutable.) "Y-m-d")
"2026-02-07"
```
## Introspection
Inspect code, namespaces, and macros. These helpers live in `phel.repl` and load automatically in the REPL and over nREPL.
### source
Return the source code of a function or macro as a string:
```phel
user:1> (source filter)
"(defn filter\n \"Returns a lazy sequence of elements where predicate returns true...\"\n [pred & args]\n ...)"
```
### find-fn
Search functions by name or docstring. Returns a vector of maps with `:ns`, `:name`, `:doc`, and arity info:
```phel
user:1> (find-fn "reduce")
@[{:ns "phel.core", :name "reduce", :doc "...", :private false, :min-arity 3, :max-arity 3, :is-variadic false}
...]
```
### symbol-info
Structured metadata for a symbol: docs, source location, arity, namespace:
```phel
user:1> (symbol-info map)
{:doc "...", :file ".../seq-fns.phel", :line 54, :min-arity 1, :is-variadic true, :ns "phel.core", :name "map"}
```
### Namespace introspection
Inspect namespaces:
```phel
user:1> (ns-publics 'phel.core)
; Returns all public definitions in the namespace
user:2> (ns-aliases 'my.app)
; Returns all namespace aliases
user:3> (ns-refers 'my.app)
; Returns all referred symbols
user:4> (ns-list)
; Returns all loaded namespaces
user:5> (ns-interns 'my.app)
; Returns all interned vars in the namespace
```
### Namespace manipulation
Create, find, remove namespaces and intern vars at runtime (`phel.repl`):
```phel
(find-ns 'my.app) ; => namespace or nil
(create-ns 'my.scratch) ; create and return
(intern 'my.scratch 'answer 42) ; intern a var
(remove-ns 'my.scratch)
```
### Macro expansion
Expand macros to see generated code:
```phel
user:1> (macroexpand-1 '(defn foo [x] x))
; Expands one level of macro
user:2> (macroexpand '(defn foo [x] x))
; Fully expands all macros
```
### Evaluation
Evaluate code from strings or files:
```phel
user:1> (eval-str "(+ 1 2)")
3
user:2> (load-file "src/my/app.phel")
; Loads and evaluates an entire file
```
### Interactive testing
Run tests for a namespace from the REPL:
```phel
user:1> (require phel.repl :refer [test-ns])
user:2> (test-ns "my-app.tests")
; Runs all tests in the namespace and prints results
```
`phel.repl` also exposes `run-tests` and `run-test`, which load the namespace first if needed: `run-tests` takes one or more namespace symbols, `run-test` a single fully qualified test symbol:
```phel
user:3> (run-tests 'my-app.users-test 'my-app.handlers-test)
user:4> (run-test 'my-app.users-test/creates-a-user)
```
See also [Testing](/documentation/testing/) for `reset-stats`, `get-stats`, and `restore-stats`.
## Auto-injected utilities
`(in-ns ...)` auto-injects `doc`, `require`, `use` into the new namespace. No manual imports.
```phel
user:1> (in-ns 'my.app)
my.app:2> (doc map)
; Works immediately: no require needed
```
## REPL-driven workflow
Use the REPL as your primary feedback loop, not just one-off tests.
### Explore data interactively
Build transformations step by step, verifying each stage:
```phel
user:1> (def users [{:name "Alice" :role :admin}
....:2> {:name "Bob" :role :user}
....:3> {:name "Carol" :role :admin}])
user:4> (filter #(= :admin (:role %)) users)
@[{:name "Alice", :role :admin} {:name "Carol", :role :admin}]
user:5> (map :name *1)
@["Alice" "Carol"]
```
### Test functions as you write them
Define, test, refine, repeat:
```phel
user:1> (defn fizzbuzz [n]
....:2> (cond
....:3> (= 0 (% n 15)) "FizzBuzz"
....:4> (= 0 (% n 3)) "Fizz"
....:5> (= 0 (% n 5)) "Buzz"
....:6> :else n))
user:7> (fizzbuzz 15)
"FizzBuzz"
user:8> (fizzbuzz 7)
7
user:9> (map fizzbuzz (range 1 16))
@[1 2 "Fizz" 4 "Buzz" "Fizz" 7 8 "Fizz" "Buzz" 11 "Fizz" 13 14 "FizzBuzz"]
```
### Reload changed code
Edit files in your editor and pull the changes into the running REPL without restarting it. `(reload!)` re-evaluates only project namespaces whose source changed since the last load, plus their dependents, in dependency order:
```phel
user:1> (reload!)
; => @[my-app.users my-app.handlers] ; reloaded the changed ns and what depends on it
user:2> (reload-all!)
; => force-reloads every loaded project namespace, ignoring mtimes
```
`reload!`, `reload-all!`, `run-tests`, and `run-test` live in `phel.repl` and load automatically in the REPL and over nREPL. Editors can bind the matching nREPL ops to editor commands: see [Editor Support](/documentation/tooling/editor-support/#nrepl-and-editor-integration).
### Explore PHP interop
Try PHP functions and classes interactively:
```phel
user:1> (use DateTimeImmutable)
user:2> (def now (DateTimeImmutable.))
user:3> (.format now "l, F j, Y")
"Saturday, February 7, 2026"
user:4> (-> now (.modify "+3 days") (.format "Y-m-d"))
"2026-02-10"
user:5> (php/json_encode (php/array 1 2 3))
"[1,2,3]"
```
### Inspect data structures
See persistent data structures in action:
```phel
user:1> (def m {:a 1 :b 2 :c 3})
user:2> (assoc m :d 4)
{:a 1, :b 2, :c 3, :d 4}
user:3> m
{:a 1, :b 2, :c 3} ; Original unchanged!
user:4> (type m)
:hash-map
user:5> (keys m)
[:a :b :c]
user:6> (vals m)
[1 2 3]
```
## Debug helpers
Stdlib ships helpers for inspecting values during development.
### Global tap system
Routes debug values to handlers. `tap>` invokes every function registered via `add-tap`.
Since 0.49 the REPL registers `phel.repl/print-tap` on startup, so `(tap> x)` prints `tap> x` at the prompt with no setup; run `(remove-tap phel.repl/print-tap)` to silence it.
`tap>` sends a value to every registered handler and returns `true`:
```phel
(tap> {:event :user-login :user-id 42})
```
`add-tap` / `remove-tap` register or unregister a handler function:
```phel
(defn my-logger [value]
(println "TAP:" value))
(add-tap my-logger)
(tap> "hello") ; Prints: TAP: hello
(remove-tap my-logger)
```
Exceptions in individual taps are swallowed so one bad handler doesn't break others.
Collect tapped values during a test:
```phel
(def tapped (atom []))
(def collector (fn [v] (swap! tapped conj v)))
(add-tap collector)
(tap> {:step 1 :result "ok"})
(tap> {:step 2 :result "fail"})
(deref tapped)
;; => [{:step 1, :result "ok"} {:step 2, :result "fail"}]
(remove-tap collector)
```
### Pretty printing
`phel.pprint` provides `pprint` and `pprint-str` for readable nested data output:
```phel
(ns my-app
(:require phel.pprint :refer [pprint]))
(pprint {:users [{:name "Alice" :roles [:admin :editor]}
{:name "Bob" :roles [:viewer]}]
:count 2})
;; Prints:
;; {:users [{:name "Alice", :roles [:admin :editor]}
;; {:name "Bob", :roles [:viewer]}]
;; :count 2}
```
`pprint-str` returns the formatted string instead of printing it.
### PHP native inspection
Phel values are PHP objects, so every PHP inspection function works via `php/`: `(php/var_dump x)`, `(php/print_r ...)`, and Symfony VarDumper's `(php/dump ...)` / `(php/dd ...)`. See [PHP Debugging Tools](/documentation/tooling/php-tools/) for examples and setup.
## Tips
- **Use `doc` liberally:** faster than the browser.
- **Build expressions incrementally:** start simple, verify, compose.
- **Copy working expressions into source files:** the REPL is a scratchpad.
- **Use `require` to load your modules:** test your code live.
- **`Ctrl-C` cancels current input** if stuck mid-expression.
## Next steps
- [Debugging](/documentation/debugging/) - the full debugging workflow: `dbg`, stack traces, Xdebug, profiling
- [CLI commands](/documentation/tooling/cli-commands/) - run, test, and build from the terminal
- [Editor support](/documentation/tooling/editor-support/) - get the same eval loop inside your editor via `phel nrepl`
- [Testing](/documentation/testing/) - run and inspect tests from the REPL
---
# Testing
> Source: https://phel-lang.org/documentation/testing/
Built-in unit testing with no boilerplate. Define tests as functions, run them from the CLI.
## Quick start
```phel
(ns my-app.math-test
(:require phel.test :refer [deftest is]))
(deftest addition-works
(is (= 4 (+ 2 2))))
(deftest string-concat
(is (= "hello world" (str "hello" " " "world")))
(is (not (= "" (str "a" "b")))))
```
Run:
```bash
./vendor/bin/phel test
```
Output:
```
....
2 tests, 3 assertions, 0 failures.
```
No class boilerplate. Tests are plain functions:
```php
// PHPUnit
class MathTest extends TestCase {
public function testAddition() {
$this->assertEquals(4, 2 + 2);
}
}
// Phel
(deftest addition-works
(is (= 4 (+ 2 2))))
```
## Assertions
The `is` macro defines assertions. Optional second argument is a description string shown on failure.
```phel
(ns my-app.is-test
(:require phel.test :refer [deftest is]))
(deftest assertions
(is (= 4 (+ 2 2)))
(is (= 4 (+ 2 2)) "2 + 2 should be 4"))
```
### Equality and predicates
```phel
(is (= expected actual)) ; equality
(is (true? value)) ; predicate
(is (not (= "x" (str "a" "b")))) ; negation
(is (nil? (get {} :missing))) ; any predicate works
```
For collection equality, failures render a unified diff so missing/extra entries are obvious:
```
FAIL (= a b)
--- expected
+++ actual
[:a 1
- :b 2
+ :b 99
:c 3]
```
### Exceptions
```phel
(ns my-app.exception-test
(:require phel.test :refer [deftest is]))
(deftest exception-assertions
;; assert throws
(is (thrown? Exception
(throw (php/new Exception "test"))))
;; assert throws with specific message
(is (thrown-with-msg? Exception "test"
(throw (php/new Exception "test")))))
```
### Output
```phel
(ns my-app.output-test
(:require phel.test :refer [deftest is]))
(deftest output-assertion
;; assert what gets printed to stdout
(is (output? "hello" (print "hello"))))
```
Exception testing more concise than PHPUnit:
```php
// PHPUnit
$this->expectException(Exception::class);
throw new Exception("test");
// or
$this->expectException(Exception::class);
$this->expectExceptionMessage("test");
throw new Exception("test");
// Phel (inline exception assertions)
(is (thrown? Exception (throw (php/new Exception "test"))))
(is (thrown-with-msg? Exception "test" (throw (php/new Exception "test"))))
```
The `output?` assertion is similar to PHPUnit's output buffering:
```php
// PHPUnit
$this->expectOutputString("hello");
echo "hello";
// Phel
(is (output? "hello" (print "hello")))
```
## Defining tests
`deftest` defines a test. Each test can contain any number of `is` assertions. A test passes when all assertions pass.
```phel
(ns my-app.cart-test
(:require phel.test :refer [deftest is])
(:require my-app.cart :refer [add-item total]))
(deftest empty-cart-has-zero-total
(is (= 0 (total []))))
(deftest add-item-increases-total
(let [cart (add-item [] {:price 10 :qty 2})]
(is (= 20 (total cart)))
(is (= 1 (count cart)))))
(deftest rejects-negative-price
(is (thrown? Exception (add-item [] {:price -5 :qty 1}))))
```
## Running tests
Run via `./vendor/bin/phel test`. Picks up tests recursively from [withTestDirs](/documentation/configuration/), defaults to `tests/`.
Pass filenames to run specific files:
```bash
./vendor/bin/phel test tests/main.phel tests/utils.phel
```
Filter by name with `--filter`:
```bash
./vendor/bin/phel test tests/utils.phel --filter my-test-function
```
Stop on first failure with `--fail-fast`:
```bash
./vendor/bin/phel test --fail-fast
```
Print discovered tests without running them (`--list`), re-run only failures from the previous run (`--last-failed`), or print the N slowest tests after the summary (`--slowest=N`):
```bash
./vendor/bin/phel test --list
./vendor/bin/phel test --last-failed
./vendor/bin/phel test --slowest=10
```
`--last-failed` persists failures to `.phel/last-failed.txt`.
`--testdox` for TestDox format. `--quiet` for errors only, `--silent` to silence fully.
Full options: `./vendor/bin/phel test --help`.
### Reporters
Pick format with `--reporter=`. Repeatable for multiple formats.
| Reporter | Description |
|-------------|---------------------------------------------|
| `default` | Human-readable summary (default) |
| `testdox` | Sentence-style names |
| `dot` | One character per test |
| `tap` | Test Anything Protocol |
| `junit-xml` | JUnit XML (use `--output=path` for a file) |
```bash
./vendor/bin/phel test --reporter=dot
./vendor/bin/phel test --reporter=junit-xml --output=build/tests.xml
./vendor/bin/phel test --reporter=tap --reporter=junit-xml --output=build/tests.xml
```
`phel.test/report` is a multimethod dispatching on event `:type`. Register custom reporters from Phel.
### Selectors
Filter by tag, namespace glob, or regex:
```bash
./vendor/bin/phel test --include=integration
./vendor/bin/phel test --exclude=slow
./vendor/bin/phel test --ns='my-app.http.*'
./vendor/bin/phel test --filter 'user.*login'
```
Tag tests with metadata:
```phel
(deftest ^:integration full-signup-flow
...)
(deftest ^{:tags [:integration :slow]} heavy-job
...)
```
Skipped tests emit `:skipped` event.
### Repeat and random order
Re-run each test N times, randomize discovery order, and seed for reproducible runs:
```bash
./vendor/bin/phel test --repeat=10 # stress a flaky test
./vendor/bin/phel test --random-order # random order, random seed
./vendor/bin/phel test --random-order --seed=42 # deterministic
```
`--seed=` alone fixes the seed for the default deterministic order.
### Parallel execution
Run namespaces across subprocess workers to speed up large suites:
```bash
./vendor/bin/phel test --parallel=auto # CPU detection, capped at 8 workers
./vendor/bin/phel test --parallel=4 # fixed worker count
./vendor/bin/phel test --parallel=max # every core the kernel reports
```
Auto-disabled for `--reporter=tap`, `--list`, and when a profiler hook is installed.
### Watch mode
Re-run the selected tests on every change to a `.phel` file or `phel-config.php` under the project source and test directories. Combine it with selectors to tighten the loop to what you are working on:
```bash
./vendor/bin/phel test --watch
./vendor/bin/phel test --watch --ns=my-app.users.* # only this namespace
```
Press `Ctrl+C` to stop. A failed `=` assertion prints an expected/actual diff with a caret at the first difference, and `FAIL`/`ERROR` headlines carry the failing `deftest` name and location (`FAIL my-test (file.phel:4)`).
### Re-run failures
After a run, re-run only the tests that failed instead of the whole suite. The failing set is read from `/last-failed.txt`:
```bash
./vendor/bin/phel test --last-failed
./vendor/bin/phel test --last-failed --repeat=20 # hammer the flaky ones
```
### Coverage
Collect line coverage mapped back to your `.phel` sources. Requires the `pcov` or `xdebug` extension (you get a clear error otherwise), and runs serially: `--parallel` is disabled for the run. Only project source files count; vendor and core are excluded.
```bash
./vendor/bin/phel test --coverage # per-file + total %, as text
./vendor/bin/phel test --coverage=clover \
--coverage-output=coverage.xml # Clover XML for CI (Codecov etc.)
```
Test command similar to PHPUnit:
```bash
# PHPUnit
./vendor/bin/phpunit tests/
./vendor/bin/phpunit tests/MainTest.php
./vendor/bin/phpunit --filter testMyFunction
# Phel
./vendor/bin/phel test
./vendor/bin/phel test tests/main.phel
./vendor/bin/phel test --filter my-test-function
```
Both support filtering, verbose output, specific files.
Run tests from Phel code with `run-tests`. Takes options map (can be empty) and one or more namespaces.
```phel
(ns my-app.runner
(:require phel.test :refer [run-tests]))
(run-tests {} 'my.ns.a 'my.ns.b)
```
### Interactive testing with `test-ns`
Run tests for a single namespace from the REPL:
```phel
(ns my-app.tests
(:require phel.test :refer [deftest is])
(:require phel.repl :refer [test-ns]))
; Run all tests in a namespace (pass namespace as a string)
(test-ns "my-app.tests")
```
Useful for REPL-driven feedback without running the full suite.
### Test statistics
Manage stats programmatically:
```phel
; Reset test counters to zero
(reset-stats)
; Get current test statistics (pass/fail/error counts)
(get-stats)
; Save and restore stats around a test run
(def saved (get-stats))
(test-ns "my-app.tests")
(restore-stats saved)
```
Useful in REPL to isolate or reset state between runs.
## Mocking
`phel.mock` module replaces functions with test doubles.
### Creating mocks
```phel
(ns my-app.tests
(:require phel.test :refer [deftest is])
(:require phel.mock :refer [mock mock-fn mock-returning mock-throwing
calls call-count called? called-with?
called-once? never-called? reset-mock!
with-mocks]))
;; Fixed return value
(def my-mock (mock :ok))
(my-mock "any" "args") ; => :ok
;; Custom behavior
(def double-mock (mock-fn #(* % 2)))
(double-mock 5) ; => 10
;; Consecutive return values
(def seq-mock (mock-returning [1 2 3]))
(seq-mock) ; => 1
(seq-mock) ; => 2
(seq-mock) ; => 3
;; Mock that throws
(def err-mock (mock-throwing (php/new RuntimeException "fail")))
```
### Inspecting calls
```phel
(ns my-app.mock-test
(:require phel.mock :refer [mock calls call-count called?
called-with? called-once? never-called?]))
(def m (mock :result))
(m "a" "b")
(m "c")
(calls m) ; => [["a" "b"] ["c"]]
(call-count m) ; => 2
(called? m) ; => true
(called-with? m "a" "b") ; => true
(called-once? m) ; => false
(never-called? m) ; => false
```
### Replacing functions in tests
`with-mocks` temporarily replaces functions via dynamic binding. Auto-resets after the block:
```phel
(ns my-app.with-mocks-test
(:require phel.test :refer [deftest is])
(:require phel.mock :refer [mock with-mocks called-once?]))
(defn fetch-user [id]
;; ... makes HTTP call ...
)
(deftest test-with-mock
(with-mocks [fetch-user (mock {:id 1 :name "Alice"})]
(is (= {:id 1 :name "Alice"} (fetch-user 42)))
(is (called-once? fetch-user))))
```
Simpler than Mockery or PHPUnit mocks:
```php
// PHPUnit
$mock = $this->createMock(UserService::class);
$mock->method('find')->willReturn(['id' => 1]);
// Phel
(with-mocks [find-user (mock {:id 1})]
(find-user 42))
```
No class structure. Mock any function directly.
## Property-based testing
Instead of writing specific examples, describe properties that must hold for *any* input. Phel generates random inputs and shrinks failures to the smallest reproducing case.
```phel
(ns my-app.tests
(:require phel.test :refer [deftest is])
(:require phel.test.gen :as gen :refer [defspec]))
;; Property: reversing twice gives back the original (holds for any vector of ints)
;; Shape: (defspec name options args-gen property-fn)
(defspec reverse-roundtrip
{}
(gen/tuple (gen/vector-of gen/int))
(fn [xs] (= xs (reverse (reverse xs)))))
;; Property: sorting is idempotent (sort of a sorted list is still sorted)
(defspec sort-idempotent
{}
(gen/tuple (gen/vector-of gen/int))
(fn [xs]
(let [sorted (sort xs)]
(= sorted (sort sorted)))))
```
On failure, Phel shrinks the input to the smallest case that still fails, then reports `:shrunk-args`, `:original-args`, `:shrink-steps`, and a `:seed` to reproduce the run.
Available generators: `gen/int`, `gen/string`, `gen/boolean`, `gen/keyword`, `gen/tuple`, `gen/vector-of`, `gen/map-of`, `gen/one-of`, `gen/frequency`, `gen/such-that`, and more in [phel.test.gen](/documentation/reference/api/test-gen/).
Opt out of shrinking with `^:no-shrink` metadata or `:shrink? false`.
## Next steps
- [Configuration](/documentation/configuration/): point `withTestDirs` at your test folders.
- [CLI Commands](/documentation/tooling/cli-commands): the full `phel test` flag list.
- [phel.test API](/documentation/reference/api/test): every assertion and helper.
- [Debugging](/documentation/debugging/): find the bug before you pin it with a test.
---
# Error Reference
> Source: https://phel-lang.org/documentation/reference/errors/
> This page is the canonical index of compiler error **codes**. To learn how to `throw`, `catch`, and attach data to errors in your own code, see [Error handling](/documentation/language/error-handling/).
Phel compiler errors are tagged with a stable code like `[PHEL001]`. The code
survives wording changes, so it is the reliable thing to search for. An error
prints as the code, a message, the source location, a snippet of the offending
code, and often a hint:
```text
[PHEL001] Cannot resolve symbol 'maap'. Did you mean 'map'?
in src/app.phel:12
```
Codes are grouped by the compiler stage that raises them.
## Analyzer errors
Raised while analyzing forms: undefined names, wrong arity, type and binding problems. The bulk of day-to-day errors.
### PHEL001 : Undefined symbol
A symbol could not be resolved to a definition in the current scope.
**Common cause:** A typo, a missing `(:require ...)` for the namespace the symbol lives in, an alias that does not match, or using a binding before it is defined.
**Fix:** Check the spelling, require the namespace (e.g. `(:require phel\string :as str)` for `str/...`), or move the definition above its first use. The error message suggests near matches.
### PHEL002 : Arity error
A function was called with the wrong number of arguments.
**Common cause:** The call site passes more or fewer arguments than any of the function's arities accept.
**Fix:** Match the call to a declared arity. For variadic functions use `& rest` in the parameter vector.
### PHEL003 : Type error
A form received a value of the wrong type.
**Common cause:** For example attaching metadata that is not a String, Keyword or Map, or passing a non-collection where a collection is required.
**Fix:** Pass the type the form expects. The message names the value it got.
### PHEL004 : Def not allowed
`def` was used somewhere it is not allowed.
**Common cause:** `def` defines a top-level var, so it cannot appear nested inside a function body or another expression.
**Fix:** Move the `def` to the top level of the namespace. For function-local values use `let`.
### PHEL005 : Macro expansion error
A macro threw while expanding.
**Common cause:** The macro received arguments it did not expect, or its own body raised during expansion.
**Fix:** Check the arguments at the call site and inspect the expansion with `(macroexpand '(your-form ...))`.
**Learn more:** [Macros](/documentation/language/macros/).
### PHEL006 : Inline expansion error
An inline-expanded function failed to expand.
**Common cause:** A function declared with an `:inline` implementation produced an invalid expansion for the given call.
**Fix:** Call the function within the shape its `:inline` definition supports, or report it upstream if it is a core function.
### PHEL007 : Invalid special form
A special form was written in an invalid shape.
**Common cause:** A core form such as `if`, `let`, `fn`, `do` or `quote` was given the wrong structure (missing or extra parts).
**Fix:** Match the form's grammar, e.g. `(if test then else?)`, `(let [bindings*] body*)`.
### PHEL008 : Binding error
A binding vector is invalid.
**Common cause:** An odd number of binding forms in `let`/`loop`, or a binding target that cannot be destructured.
**Fix:** Provide an even number of `name value` pairs and use valid destructuring targets (symbols, vectors, maps).
**Learn more:** [Destructuring](/documentation/language/destructuring/), [Global and local bindings](/documentation/language/global-and-local-bindings/).
### PHEL009 : Interface error
An interface or protocol definition (or its implementation) is invalid.
**Common cause:** A malformed `definterface`/`defprotocol`, or trying to implement a `defprotocol` inline in `defstruct` (only `definterface` can be implemented inline).
**Fix:** Use `definterface` for inline implementation, or `defprotocol` plus `extend-type` per struct.
**Learn more:** [Interfaces](/documentation/language/interfaces/).
### PHEL010 : Recur error
`recur` was used incorrectly.
**Common cause:** `recur` appeared outside a `loop`/`fn` tail position, or with an argument count that does not match the recursion point.
**Fix:** Use `recur` only in tail position, with as many arguments as the enclosing `loop`/`fn` binds.
**Learn more:** [Functions and recursion](/documentation/language/functions-and-recursion/).
### PHEL011 : Not callable
A value that is not a function was called.
**Common cause:** A non-callable value (a number, string, keyword used wrongly) sits in the head position of a list, often an extra pair of parentheses.
**Fix:** Remove the stray parentheses, or put a function in the call position.
## Parser errors
Raised while parsing tokens into forms, almost always an unbalanced or unterminated bracket.
### PHEL100 : Unterminated list
A list was not closed.
**Common cause:** A missing `)`.
**Fix:** Balance the parentheses. Editor rainbow-brackets or `phel format` help spot it.
### PHEL101 : Unterminated vector
A vector was not closed.
**Common cause:** A missing `]`.
**Fix:** Balance the brackets.
### PHEL102 : Unterminated map
A map was not closed.
**Common cause:** A missing `}`, or an odd number of key/value forms.
**Fix:** Close the brace and ensure every key has a value.
### PHEL103 : Unterminated table
A table literal was not closed.
**Common cause:** A missing closing brace on a `@{ ... }` table literal.
**Fix:** Close the table literal.
### PHEL110 : Unexpected token
A token appeared where the parser did not expect one.
**Common cause:** A stray closing bracket, or a reader macro applied to nothing.
**Fix:** Remove or complete the offending token.
### PHEL120 : Parser error
A general parser error.
**Common cause:** The token stream could not be assembled into valid forms for a reason not covered by a more specific code.
**Fix:** Check the indicated location for malformed structure.
## Reader errors
Raised while reading quote / quasiquote forms.
### PHEL200 : Invalid quote
A `quote` form is malformed.
**Common cause:** `quote` was given the wrong number of arguments.
**Fix:** Use `(quote x)` or the `'x` shorthand with a single form.
### PHEL201 : Invalid unquote
An unquote (`~`) is invalid.
**Common cause:** `~` was used outside a quasiquote (`` ` ``) or with a wrong argument shape.
**Fix:** Only use `~` inside a quasiquoted form.
### PHEL202 : Invalid splice
A splicing unquote (`~@`) is invalid.
**Common cause:** `~@` was used outside a quasiquote, or in a position where a sequence cannot be spliced.
**Fix:** Use `~@` inside a quasiquote, splicing into a list or vector.
### PHEL210 : Reader error
A general reader error.
**Common cause:** A reader macro could not be read for a reason not covered by a more specific code.
**Fix:** Check the quote/quasiquote forms at the indicated location.
## Lexer errors
Raised while turning source text into tokens: invalid characters or unterminated strings.
### PHEL300 : Invalid character
An invalid character was found in the source.
**Common cause:** A character that is not valid Phel syntax at that position.
**Fix:** Remove or escape the character.
### PHEL301 : Unterminated string
A string was not closed.
**Common cause:** A missing closing `"`, sometimes from an unescaped quote inside the string.
**Fix:** Close the string and escape interior quotes as `\"`.
### PHEL310 : Lexer error
A general lexer error.
**Common cause:** The source could not be tokenized for a reason not covered by a more specific code.
**Fix:** Check the indicated location for stray or invalid characters.
---