Skip to main content

Release: 0.53 - Floor Raised

Latest update

0.53.0 - Floor Raised

Released 2026-09-24 · GitHub release

PHP 8.5 minimum + 2-4x faster assoc/get-in/update-in + short type tags (^map, ^vector) + phel bench --ab

⚖️ Changed

  • BREAKING: PHP 8.5 is the minimum. The floor moves now: once 1.x starts, raising it needs a major. (#3302)
  • BREAKING (PHP API): FilesystemFacadeInterface moves from Phel\Filesystem to Phel\Shared\Facade. Phel\Fiber\FiberFacadeInterface is gone: type-hint Phel\Fiber\FiberFacade instead. (#2859)
  • BREAKING (PHP API): MetaInterface::withMeta() always returns a copy. Symbols, keywords, atoms and functions are no longer changed in place, so keep the returned value. For atoms, use Atom::alterMeta() and Atom::resetMeta(). (#3311, ADR 0019)
  • BREAKING (PHP API): lint rule codes move to the public Phel\Shared\LintRuleCodes. The strings stay the same, so phel lint --format=json consumers keep working. Renaming a code is now a breaking change. Phel\Shared\Exceptions\ErrorCode is public too. (#3309)

🎉 Added

  • Short type tags for Phel values: ^map, ^vector, ^set, ^list, ^keyword, ^symbol and ^atom. ?map and map|null work too. (defn f [^map m] (:k m)) gets the fast ->find lookup without the full interface name. See "Type tags" in docs/spec/language-surface.md. (#3319)
  • Collection methods that return a copy are #[NoDiscard]. Dropping the result of (.put m k v), or of an inlined (conj v x) / (assoc m :b 2) outside tail position, now warns instead of doing nothing. Cast to (void) when it is deliberate. Transients are exempt, except TransientArrayMap::put, which can return a new transient. (#3303)
  • phel bench --ab=<git-ref> --pairs=N compares the working tree with a git ref, one A/B run per pair. Each row shows the mean delta, or noise when the pairs disagree on the sign. --tolerance fails only when every pair is slower by more than the tolerance. The working tree is never touched. See docs/benchmarking.md. (#3324)
  • Public PHP API, since compiled code calls it: Phel\Lang\GetIn, Phel\Lang\AssocIn and Phel\Lang\Collections\ValueIdentity. (#3320 #3321 #3328)

⚡ Performance

Literal call shapes compile to direct code, with the same results. A key list from a variable, apply, or a local that shadows the core fn keeps the runtime call.

  • (assoc m k1 v1 k2 v2 ...): one step per pair, 3.4x to 3.7x faster. (assoc! t ...) is 3.4x faster. The transient docstring now says when a transient pays off: about eight writes on a map. (#3317 #3318)
  • (get-in m [k1 k2 ...]): 2.9x faster for two keys, 4.5x for eight keys with a default. (#3320)
  • (assoc-in m [...] v) and (update-in m [...] f & args): 1.9x to 2.8x faster. (#3328)
  • At optimization level 2, (update m k (fn [v] ...)) in tail position inlines a simple literal fn: no closure, about 9% faster on a 90-key map. Off at the default level, for a :redef update, and for a body that could notice it runs in the caller's frame. (#3322) Runtime:
  • assoc of the value an index already holds returns the same vector: 2.4x faster on 100 elements. Same for rest / subvec vectors and structs. A new value costs about 3% more on a vector and 5% less on a hash map. (#3321)
  • into with a transducer, the core transducers and reduce build fewer closures. (into [] (comp (map f) (filter :alive)) coll) drops from 14.2μs to 11.8μs, (reduce + 0 v) over 32 ints from 9.6μs to 6.8μs. One change, toward Clojure: a custom transducer that calls its reducing fn with extra arguments reaches conj's variadic arity instead of throwing. (#3323)

🐛 Fixed

Compiler:

  • BREAKING: an octal escape above \377 is a compile error. It used to wrap silently: "\400" compiled to NUL. Clojure rejects it too. Char-literal errors now show file, line, snippet and caret. (#3301)
  • A bare tag after a :use import, :as alias included, resolves to the imported class. It used to reach the generated PHP unqualified and fail at call time. The tag resolves where it is declared, so a caller mapping the same alias to another class no longer infers the wrong type and throws TypeError. Union and intersection tags on defstruct and definterface resolve the same way. (#3319) Collections:
  • assoc on a map keeps a new value that is = to the stored one but not identical: -0.0 over 0.0, a BigInt over an equal int, a vector with new metadata. The unchanged check compares by identity, as Clojure does. A PHP array always counts as a change, so two recursive arrays no longer throw Nesting level too deep. Applies to every map type, transients, structs and vectors. (#3321) Tooling:
  • phel test --parallel workers run with the OPcache JIT off. A php.ini that enables the JIT, as on every GitHub runner, crashed workers with SIGBUS on arm64 macOS. A dead worker now reports its exit code or signal and its output. Also fixed: a large stderr write no longer hangs the run, stray output counts as a crash, and a namespace that kills its worker on every retry no longer stops the rest of the queue. (#3334)

👥 Contributors

@Chemaclass @JesusValeraDev

Full Changelog: v0.52.0...v0.53.0

Downloads

v0.53.0


View release on GitHub