Reader conditionals let one source file hold platform-specific code. They resolve during the parsing phase, before compilation, so the analyzer and emitter only ever see the selected form. Phel picks the :phel branch, ignores other platforms (:clj, :cljs, ...), and falls back to :default when present.
This makes .cljc files shareable between Phel, Clojure, and other Lisp dialects. If you are arriving from Clojure, the syntax is identical; see Coming from Clojure.
Selecting a form: #?()#
#?() reads keyword/form pairs and keeps exactly one:
(println
#?(:phel (php/time)
:clj (System/currentTimeMillis)
:cljs (js/Date.now)))
; In Phel this is just (php/time); the :clj and :cljs branches are dropped at parse time.Platform keys#
| Key | Platform | Matched by Phel? |
|---|---|---|
:phel | Phel | Yes |
:default | Any platform (fallback) | Yes (when no :phel) |
:clj | Clojure (JVM) | No |
:cljs | ClojureScript | No |
| any other | ignored | No |
Priority#
:phelalways wins when present, regardless of position.:defaultis the fallback when there is no:phelbranch.- If neither is present, the whole form is dropped (treated as whitespace).
(println #?(:default 0 :phel 42)) ; => 42 (:phel wins)
(println #?(:clj 99 :default 0)) ; => 0 (fallback)
;; #?(:clj 99 :cljs 88) reads as nothing and is dropped entirely.Splicing: #?@()#
#?@() splices the elements of the matched collection into the surrounding form and removes the wrapper. The selected branch must be a sequential collection (vector or list):
(println [1 #?@(:phel [2 3]) 4]) ; => [1 2 3 4]
(println (php/array 0 #?@(:phel [1 2 3]) 4)) ; => array(0, 1, 2, 3, 4)
;; Fallback and no-match behave like #?():
(println [1 #?@(:clj [8 9] :default [2 3]) 4]) ; => [1 2 3 4]
(println [1 #?@(:clj [8 9]) 4]) ; => [1 4]Top-level restriction#
#?@() is only valid inside a collection (list, vector, map, or set). Splicing at the top level is an error, because there is no parent form to splice into:
;; ERROR: Reader conditional splicing #?@() is not allowed at the top level
#?@(:phel [1 2])Use cases#
Cross-platform source files (.cljc)#
Phel discovers and compiles .cljc files alongside .phel files, so a single file can serve multiple runtimes:
;; src/shared/utils.cljc
(ns shared.utils)
(defn now []
#?(:phel (php/time)
:clj (/ (System/currentTimeMillis) 1000)))
(defn platform []
#?(:phel "phel"
:clj "clojure"
:cljs "clojurescript"
:default "unknown"))
(println (platform)) ; => "phel"
Tip: use
.as the namespace separator (shared.utils) so.cljcfiles parse cleanly under Clojure too. The legacy\separator still resolves but is deprecated.
Platform-specific dependencies#
Reader conditionals work inside the (ns ...) form, so each platform can require its own libraries:
(ns app.http
#?(:phel (:require [phel.json :as json])
:clj (:require [clojure.data.json :as json])))
(defn parse [s]
#?(:phel (json/decode s)
:clj (json/read-str s)))
Phel accepts both vector entries (
[phel.json :as json :refer [encode]]) and the list form (phel.json :as json) inside:require, so the same(ns ...)parses on both sides.
Conditional data structures#
Use splicing to add platform-specific entries to a map:
(def config
{:name "my-app"
:version "1.0"
#?@(:phel [:runtime "php" :min-version "8.4"]
:clj [:runtime "jvm" :min-version "21"])})
(println config)
; => {:name "my-app" :version "1.0" :runtime "php" :min-version "8.4"}Inside control flow#
Because conditionals resolve at parse time, they nest inside any form:
(if #?(:phel true :clj false)
(println "Running on Phel!")
(println "Running on Clojure!"))
; => "Running on Phel!"Summary#
| Syntax | Name | Behavior | Context |
|---|---|---|---|
#?() | Reader conditional | Selects one form by platform key | Anywhere |
#?@() | Reader conditional splicing | Splices collection elements into parent | Inside collections only |
Next steps#
- Namespaces - the
nsform that conditional:requireentries plug into - Cookbook -- Reader conditionals for cross-platform code - a worked
.cljcrecipe - Cheat sheet - keep it open while coding