Skip to main content

Release: 0.40 - Sharper Edges

Latest update

0.40.0 - Sharper Edges

Released 2026-05-25 · GitHub release

EDN/Transit IO + defonce + ^:by-ref params + big compiler perf wins (call-site cache, type-driven specialisation, const folding)

🎉 Added

  • phel.edn: eval-free EDN read/write (#2008)
  • phel.transit: Transit + JSON-Verbose read/write (#2009)
  • phel compile: emit PHP from a snippet, file, or stdin without evaluating. --target reserved for future ast / tokens dumps (#2043)
  • defonce special form: bind a global only when not already defined; survives REPL file reloads (#2055)
  • ^:by-ref param hint compiles to PHP &$param, so php/aset / php/array_push on (php/array) buffers propagates to the caller. ^:reference kept as alias (#2065)
  • Lexer: namespaced tagged literals (#my.app/Person)
  • CI: clojure-test-suite workflow runs against dev HEAD on every PR, non-blocking (#2036)
  • CITATION.cff for academic citation (#2016)
  • Tooling: tools/upgrade-ecosystem.sh bumps phel-lang across sibling repos; composer test-tools runs bashunit over tools/*-test.sh

Performance

Dispatch / call sites:

  • Cache global fn call sites in build mode via per-fn static $__phel_call_N slots; route known AbstractFn callees through direct ->__invoke(...) to skip magic dispatch (#2044)
  • Multi-arity fn dispatch via match (\count($args)); variadic body folds into default (#2049) Compile-time folding / hoisting:
  • Constant-fold pure phel.core arithmetic (+ - * inc dec) on literal args; short-circuit if with a literal test (#2045)
  • Hoist keyword literals to per-fn static $__phel_const_N slots; one intern-pool lookup per call site (#2046) Type-driven call specialisation (shared CallSpecialization keeps the cache scanner aligned):
  • Surface analyser :tag meta via LocalVarNode::getInferredType(); IterableTarget consumes it for ^"array" params (#2052)
  • Drop Seq::toIterable / Seq::toApplyArguments wraps when the source is already iterable or a PHP array (#2047)
  • Auto-tag ^float on params used under phel.core arithmetic / ordering wrappers (+ - * < <= > >= <=>) when a float literal sibling makes the float path inevitable. Int literals stay ambiguous to preserve BigInt / Ratio polymorphism (#2078)
  • Specialise two-arg phel.core + - * < <= > >= = to native PHP ops when both args are int / float literals or ^int|^float locals (= also accepts ^bool|^string): (+ ^int a ^int b)($a + $b) (#2079)
  • Specialise (str ...) to PHP . concat for literal and ^string args; skip the two-temp IIFE for (php/instanceof x c) between locals; specialise (:k m) to $m->find(...) when m is ^PersistentMapInterface (#2048)
  • Specialise (get coll k) to $coll->get($k) / $coll->find($k) when coll is ^PersistentVectorInterface / ^PersistentMapInterface (#2067) Runtime data structures:
  • Persistent-collection hash() memo uses a null sentinel so empty maps / sets and hash == 0 stop recomputing (#2050)
  • TransientVector::update() mutates the trie in place via a by-reference walk; PHP COW still detaches shared nodes from the persistent ancestor on first write (#2069) Emit size:
  • Collapse synthesised defn location maps into one \Phel::location(...) call (#2053)

⚖️ Changed

  • AI tooling: gitignore .<tool>/ dirs; regenerate via agnostic-ai sync from .agnostic-ai/ source (#2085)
  • BC: release tooling moved from build/ to tools/; build/ is now phar-only
  • CI: split ci.yml into quality.yml / tests.yml / smoke.yml with a shared setup-phel composite action; concurrency cancellation + least-privilege perms (#2039)
  • Compiler: hoist pure vector / map / set literals to a per-fn static cache
  • Compiler: skip Truthy::isTruthy wrap in if / ?: when the test is known-bool
  • Compiler: recur skips the $__phel_N temp shuffle when no aliasing is possible
  • phel agent-install: copy .agents/ docs by default; --with-docs replaced by --no-docs opt-out
  • phel agent-install: skip .agents/examples/ by default; opt in with --with-examples

🐛 Fixed

  • Map destructure surfaces a "Did you mean" suggestion for the Clojure-style {local :keyword} pair; :keys / :strs / :syms with a non-vector value report a shape error instead of being silently dropped (#2066)
  • ConstantScope uses SplObjectStorage::offsetExists() to silence a PHP 8.5 deprecation in emitted code
  • phel.cli: register commands via Application::addCommand() for Symfony 8 compat (#2033)
  • phel lint: cache invalidates when phel-lint.phel changes (#2027); unused-binding no longer flags symbols used in later let bindings (#2018)
  • phel doctor: auto-bootstraps temp dir; passes on fresh installs (#2020)
  • Phel::run(): backslash namespaces normalise to dot-form, matching CLI (#2021)
  • phel format: appends trailing newline (POSIX / EditorConfig) (#2022)
  • phel test: non-zero exit when --filter matches zero tests (#2023)
  • BackslashSeparatorDeprecator: stop false-warning on top-level PHP symbols with a leading-only \ (e.g. \strlen, \DateTimeInterface) (#2038)
  • Dev: patch vendored Psalm 6.16.1 on install / update for PHP 8.5 NAN-coercion crash in TLiteralFloat

👥 Contributors

@Chemaclass @JesusValeraDev

Full Changelog: v0.39.0...v0.40.0

Downloads

v0.40.0


View release on GitHub