Skip to main content

Release: 0.32 - Async Awakens

Latest update

0.32.0 - Async Awakens

Released 2026-04-12 Β· GitHub release

Async/concurrency module (AMPHP) + Clojure-compat aliases & reader syntax + defrecord/reify/sorted collections + transients & character literals.

πŸŽ‰ Added

Async & Concurrency

  • phel\async module with async, await, delay, await-all, await-any, ->closure for AMPHP-based concurrency (#793)
  • pmap for fiber-based parallel map (IO-parallel via AMPHP), matching Clojure's pmap (#793)
  • amphp/amp promoted to a runtime dependency so phel\async works out of the box, including from the PHAR (#793)
  • future macro returning a PhelFuture compatible with deref, realized?, and the @ reader shortcut; await/await-all/await-any accept both PhelFuture wrappers and raw Amp\Future values (#1287)
  • 3-arg (deref future timeout-ms timeout-val) for time-bounded blocking on a PhelFuture (#1313)
  • future-cancel, future-cancelled?, future-done? for PhelFuture lifecycle management (#1313)

Reader & Compiler

  • Character literals: \a, \space, \newline, \tab, \uNNNN, \oNNN, and punctuation forms; compile to single-char PHP strings. FQN parsing is preserved via lookahead (#1283)
  • Var-quote reader syntax #'foo, read as the bare symbol foo since Phel has no first-class Var type (#1317)
  • Symbolic number literals ##Inf, ##-Inf, ##NaN for .cljc interop (#1276)
  • Generic #<tag> tagged-literal dispatch (#cpp, #uuid, #inst, …) read as TaggedLiteralNode; unknown tags in unselected #? branches parse without error (#1277)
  • fn accepts an optional name symbol for self-recursion: (fn my-name [x] ...), including multi-arity forms (#1279, #1299)
  • Radix literals NrXXX (bases 2–36), e.g. 2r1111, 16rFF (#1281)
  • N/M-suffixed numeric literals (1N, 1.5M); suffix is stripped since Phel has no BigInt/BigDecimal, accepted for .cljc compat (#1325)
  • Dot namespace separator and Clojure aliasing for FQNs, enabling phel.core/fn, clojure.core/fn (#1251)
  • Accept . as alternate namespace separator in ns, in-ns, :require, :use (#1177)
  • Vector entries in :require, e.g. (:require [phel\str :as s :refer [upper-case]]) (#1183)
  • Automatic clojure.* β†’ phel.* namespace remapping in :require when target exists (#1207, #1210)
  • ~ and ~@ reader macros for unquote/unquote-splicing, alongside existing ,/,@ (#1201)
  • name# auto-gensym suffix inside syntax-quote, alongside existing name$ (#1195)
  • &form and &env implicit symbols in every defmacro body, enabling dialect detection via (:ns &env) (#1185)

Core Language

  • defrecord and deftype macros expanding to a defstruct plus ->Name factory (defrecord also generates map->Name); optional protocol tail is spliced into extend-type (#1324)
  • reify for anonymous protocol/interface implementation (#1226)
  • sorted-map, sorted-map-by, sorted-set, sorted-set-by for sorted persistent collections (#1228)
  • sorted? predicate for sorted-map/sorted-set (#1274)
  • 0-arg (range) returns an infinite lazy sequence starting at 0 (#1259)
  • letfn macro for mutually recursive local functions (#1224)
  • condp macro for predicate-based dispatch, including :>> result threading (#1217)
  • if-some, when-some, when-first macros for nil-aware binding (#1218)
  • assert macro for precondition checking with optional custom message (#1222)
  • *assert* var (default true) read by assert at macroexpand time; when false, assert expands to nil (#1315)
  • Nested def/defn inside another def/defn body is now permitted (#1316)
  • dotimes macro and run! function for side-effecting iteration (#1252)
  • fnil for nil-safe function wrapping with default values (#1225)
  • vary-meta for applying a function to an object's metadata (#1223)
  • min-key, max-key for finding extremes by a derived value (#1221)
  • rename-keys for renaming map keys via a key map (#1220)
  • seq? predicate for LazySeqInterface (#1231)
  • (boolean x) coercion: false for nil/false, true otherwise (#1186)
  • 1-arg (some? x) form alongside the existing 2-arg (some? pred coll) (#1184)
  • (resolve sym) globally available in phel\core (no longer requires phel\repl) (#1187)
  • :or defaults and :strs string-key support in map destructuring (#1219, #1227)
  • disj variadic function for removing keys from sets (#1285)
  • float and double coercion functions returning a PHP float (#1282)
  • object-array returning a plain PHP indexed array accessible via php/aget/php/aset (#1318)
  • array-map, aliased to hash-map since Phel has no distinct array-map type (#1319)
  • to-array for converting any collection to a plain PHP array (#1320)
  • aclone producing a shallow independent copy of a PHP array (#1321)
  • byte coercion truncating to a signed 8-bit range (-128..127) (#1327)
  • char coercion returning a single-character string from a code point or 1-char string (#1330)
  • char? predicate (single-char string, UTF-8 counted), matching ClojureScript semantics (#1334)
  • coll? predicate for persistent collections (vectors, lists, hash-maps, structs, sets, lazy-seqs) (#1336)
  • conj! transient mutator with Clojure-compatible arities over transient vectors, sets, and maps (#1338)
  • assoc!, dissoc!, disj!, pop! transient mutators, throwing InvalidArgumentException on persistent or mismatched targets (#1353)
  • some-fn higher-order predicate combinator, short-circuiting on first logical-true result (#1339)
  • counted? predicate for collections with constant-time length (lazy-seqs excluded) (#1340)
  • Single-arity (drop-last coll) equivalent to (drop-last 1 coll) (#1343)
  • NaN? as an alias for nan? (#1284)

Clojure-Compatible Aliases

  • atom, atom?, reset! as aliases for var, var?, set! (#1252)
  • persistent! as alias for persistent (#1353)
  • double? as alias for float? (#1353)
  • identical? as alias for id (#1252)
  • fn? as alias for function? (#1252)
  • map? as alias for hash-map? (#1252)
  • vals as alias for values (#1252)
  • integer? as alias for int? (#1199)
  • with-meta made public as replacement for set-meta! (#1252)

Testing

  • are macro in phel\test for template-based multiple assertions (#1255)
  • testing macro in phel\test for grouping assertions with context strings (#1237)
  • do-report in phel\test for custom assertion reporting (#1260)
  • phel\test/assert-expr is now an open multimethod, letting users extend is with custom assertion forms (#1188)
  • defmethod preserves the namespace of the multimethod, enabling cross-namespace extension with fully qualified multi-names

REPL & Tooling

  • require and use accept quoted symbols in the REPL (e.g. (require 'phel\str)) for nREPL compatibility (#1211)
  • ProjectLayout::Root for single-file / scratch projects (srcDirs = ['.']), and PhelConfig::forProject(layout: …) named-argument style with auto-detected layout when called with zero arguments (#1355)
  • phel init --minimal generates a root-layout project: single main.phel + matching main_test.phel + one-line phel-config.php at the repo root (#1355)
  • phel init now generates a matching test file by default (opt out with --no-tests) and lists phel test in the next-steps output (#1355)

Documentation

  • Clojure-to-Phel migration guide covering naming, interop, namespaces, and feature differences (#1229)
  • README "Getting Started" section showing composer require + phel init flow
  • Quickstart tutorial replaced hand-written phel-config.php with phel init; kept the manual form as an appendix

βš–οΈ Changed

  • Anonymous fn emits native PHP closures instead of AbstractFn subclasses, making them compatible with libraries that type-hint \Closure (e.g. AMPHP) without ->closure conversion (#793)
  • QuoteNode preserves the original reader-macro prefix (, vs ~, ', `, @) for faithful parser/printer round-trip (#1203)
  • Split Phel\Lang\Generators\SequenceGenerator into focused sibling generators (TransformGenerator, SliceGenerator, CombineGenerator, DedupeGenerator) (#1197)
  • Migrate all Phel source and test files from |(...) to #(...) short function syntax (#1179)
  • Migrate all in-repo Phel source, tests, and docs from ,/,@ to ~/~@ for unquote/unquote-splicing (#1203)
  • Migrate time macro in phel\core from name$ to name# auto-gensym suffix (#1203)
  • Migrate internal tests/phel/ usage of #|...|# multiline comments to ;; line comments (#1276)

⚠️ Deprecated

  • #|...|# multiline comments and bare # line comments (deprecated since v0.30, now announced for removal in v0.33); use ;; and #_ instead (#1276)
  • var β†’ atom, var? β†’ atom?, set! β†’ reset! (#1252)
  • id β†’ identical? (#1252)
  • function? β†’ fn?, hash-map? β†’ map? (#1252)
  • values β†’ vals (#1252)
  • set-meta! β†’ with-meta (#1252)
  • |(...) short function syntax with $ placeholders; use #(...) with % instead (#1179)
  • ,/,@ as unquote/unquote-splicing reader macros; use ~/~@ instead (#1203)
  • name$ auto-gensym suffix; use name# instead (#1203)

πŸ› Fixed

  • (meta x) on a local binding returns the value's metadata; only (meta 'sym) looks up definition metadata (#1352)
  • (assoc nil k v) returns a fresh map, matching Clojure (#1352)
  • assoc on a non-associative value throws InvalidArgumentException naming the received type (#1352)
  • abs throws InvalidArgumentException on non-numeric input instead of silently returning 0 (#1352)
  • drop-last no longer errors on nil: (drop-last n nil) and (drop-last nil) return [], aligning with drop/take (#1344)
  • reset!, swap!, set! now return the newly-stored value instead of nil, matching Clojure (#1304)
  • associative? returns true for vectors and PHP indexed arrays, matching Clojure's Associative protocol (#1303)
  • (vec map) returns entries as 2-element vectors (e.g. (vec {:a 1}) => [[:a 1]]) instead of just values (#1305)
  • min-key/max-key return the latest argument on ties, matching Clojure's reference implementation (#1306)
  • 1-arg (thrown? body) defaults to catching any \Throwable, matching Clojure's portability convention (#1307)
  • -0x8000000000000000 (and -0b..., -0o... at 64-bit minimum) parses correctly to PHP_INT_MIN instead of crashing with ParseError (#1278)
  • are macro no longer eagerly evaluates list literals in table cells; cells are substituted symbolically, matching clojure.template/do-template (#1280)
  • = on lazy sequences no longer realizes an infinite side: LazySeq/ChunkedSeq/AbstractPersistentVector short-circuit on identity and walk pairwise via lockstep iterators (#1286)
  • (php/yield ...) in return position no longer emits return yield ...;, which broke PHP generator semantics (#793)
  • phel run no longer buffers output so println/print flush immediately, fixing silent output in long-running AMPHP servers (#793)
  • REPL require supports dot namespace separator and Clojure aliasing, e.g. (require phel.str), (require clojure.str) (#1263)
  • REPL (require 'foo) throws RuntimeException when the namespace cannot be found instead of silently succeeding (#1246)
  • PHP reserved keywords (e.g. and, list, class) allowed in namespace names, matching PHP 8.0+ (#1230)
  • macroexpand/macroexpand-1 are now functions (were macros), so quoted forms expand correctly and unquoted forms evaluate eagerly (#1209)
  • macroexpand no longer applies inline expansion to non-macro forms: (macroexpand '(+ 1 2)) returns (+ 1 2) (#1208)
  • Lexer no longer swallows a reader conditional (#?(...)) following a gensym-suffixed symbol (#1195)
  • Lexer accepts ' inside and at the end of symbol names (e.g. a', foo'', a'b); leading ' is still the quote reader macro (#1275)
  • php/... calls to namespaced PHP functions (e.g. php/Amp\File\write) emit fully qualified names so they resolve from compiled/cached files (#1180)
  • phel.phar no longer emits duplicate-namespace warnings or fails to write the compiled-code cache when run from a directory without phel-config.php (#1354)

πŸ‘₯ Contributors

@Chemaclass @JesusValeraDev @MattVanHorn @jasalt

Full Changelog: v0.31.0...v0.32.0

Downloads

v0.32.0


View release on GitHub