Skip to main content

Release: 0.48 - Step Into

Latest update

0.48.0 - Step Into

Released 2026-07-15 · GitHub release

New core fns (trampoline, reductions, subvec, with-open, dbg) + step debugger + HTML coverage + read-only fixes & compiler speedups

🎉 Added

  • reduce-kv: reduce an associative collection with (f acc key value); vector indices act as keys, reduced short-circuits (#2680)
  • gcd and lcm: greatest common divisor and least common multiple of integers (#2686)
  • trampoline: stack-safe mutual recursion by bouncing returned thunks (#2682)
  • reductions: lazy sequence of a reduce's intermediate values, with or without init (#2681)
  • subvec: bounds-checked persistent vector slice; non-integer bounds truncate toward zero and NaN counts as 0, matching Clojure (#2683)
  • arity and variadic?: reflect a function's required parameter count and variadic flag (#2685)
  • inspect: pretty-prints a value (colored on TTY) and returns it unchanged, for pipeline debugging (#2688)
  • with-open: scoped-cleanup macro; closes bound resources in reverse order, even on exception (#2684)
  • phel test --coverage=html: self-contained HTML coverage report with line-colored .phel sources (#2692)
  • phel export stubs now carry native parameter/return types and @param/@return docblocks from :tag metadata (#2695)
  • dbg: debug macro printing [file:line] form => value to stderr, returning the value (#2687)
  • (break): stepping-debugger foundation, pauses at the call site and opens a sub-REPL over the captured locals; (continue) or stdin EOF resumes, so non-interactive runs never hang (#2690)

🐛 Fixed

  • phel no longer fatals in read-only / unwritable environments (e.g. the NixOS build sandbox); --help, doc, eval, run, compile, and the REPL all work from an unwritable project root (nixpkgs 0.47.0 versionCheckHook failure):
    • caches degrade quietly instead of aborting, Gacela caches (via Gacela 1.16), build/intermediate caches, and the temp-dir/error-log writers skip writes without raw PHP warnings; a pre-warmed cache is still read
    • the OPcache re-exec skips a read-only file-cache dir instead of letting PHP abort at startup
    • phel doc inside a PHAR falls back to the system temp dir when the working directory is unwritable
    • CLI commands fail loudly, not silently: phel format skips an unreadable directory instead of aborting; phel format, phel test --coverage, and phel profile --output report a clear error when a target file can't be written; phel run --debug warns once and keeps running on an unwritable log (it looped silent failed writes before)
  • symfony/polyfill-mbstring is now an explicit dependency, so minimal PHP builds without ext-mbstring keep working
  • Distributed PHAR no longer bundles example templates' build artifacts, phel init --template shipped any leftover .phel/ cache and vendor/, bloating a release PHAR from ~2 MB to ~14 MB; only template sources ship now (#2678)
  • phel test --help now shows --parallel=auto in its example; the bare --parallel it printed before errors, since the option needs a value (an integer, auto, or max) (#2679)
  • get on a :tag-typed vector with an out-of-range or non-integer key now returns nil (or the supplied default), matching runtime get, instead of throwing, the compiler no longer lowers it to an unguarded $v->get() (#2712)
  • The compiled-code cache no longer serves stale PHP after a compiler-only change: its per-source key hashes just the .phel source, so an emitter/analyzer change that alters output for unchanged source (e.g. the cross-fn return-type :tag inference) could keep serving a pre-change compiled file within the same version. The cache index format version is bumped, invalidating stale entries once and forcing a cold recompile (#2732)
  • Phel no longer triggers PHP 8.3+ deprecation warnings, ReflectionType string casts now call __toString() (removed in PHP 9.0), keeping phel export, LSP, and PHP-interop reflection warning-free on PHP 8.3–8.5

Performance

  • (reduce f init v) where v is a local tagged as a PersistentVectorInterface now compiles to a native foreach over the vector with the step function hoisted out of the loop (resolved once, invoked directly per element) and the accumulator held in a plain PHP variable. The runtime phel.core/reduce boxes the accumulator and an early-exit flag in two Volatile objects and walks the collection with a generic dofor, paying a deref/reset pair plus a reduced? dispatch on every element; the lowering removes all of it. The (reduced x) early-termination contract is preserved, a Reduced result unwraps into the accumulator and breaks the loop. The 2-arity (reduce f coll) is untouched (it seeds from the collection and calls (f) on empty input), as are map and filter, which are lazy and must stay lazy (#2714)
  • (** x 2) where x is a local tagged int or float now compiles to a native ($x * $x) instead of a NumericOperations::power call, ~2.3x faster on PHP 8.5, since squaring is the common exponent and the runtime path is a real function call. Only the integer literal exponent 2 on a tagged local reduces: an untagged base could be a Ratio/BigDecimal at runtime (whose power and multiply paths are not interchangeable), a float 2.0 exponent takes power's float branch, and the base is emitted twice so it must be a plain variable. The lowering emits the same native * a tagged (* x y) already does, so a tagged square inherits that operator's overflow behavior (overflows to float rather than promoting to BigInt); untagged ** is unchanged (#2721)
  • A foreach (and the dofor/doseq macros built on it) in return position, a loop as a function's tail, now emits the loop as plain statements followed by return null; instead of wrapping it in an immediately-invoked closure (function() use (…) { … })(), saving a closure allocation and call frame on every call to that function; the tracing JIT benefits most. Expression-context loops keep the wrapper (they must yield a value inline), and a loop whose body contains php/yield keeps it too, so the enclosing function is never silently promoted to a generator (#2716)
  • let/loop bindings whose init calls a pure PHP built-in with a fixed primitive return type (php/cos, php/sqrt, php/ord, php/implode, and ~35 more trig/math/string/ctype_* functions) now infer that type, so arithmetic, comparison, and string ops on the binding emit native PHP operators instead of runtime dispatch, matching what already happened when the call was written inline as an operand. Argument-typed functions (pow, str_replace, json_encode) stay excluded so no wrong tag is stamped (#2712)
  • def/defn location metadata emits a single \Phel::locationMeta(...) call instead of an expanded keyword-keyed map; extra metadata (:doc, :private, :tag, …) folds in as trailing args, so docstring-heavy namespaces shrink too. Runtime map stays value-equal (#2722, #2725)
  • Equal collection literals ([1 2 3], {:a 1}, sets) repeated in a function body share one cached constant slot instead of one per occurrence, shrinking emitted PHP (#2720)
  • A fn/defn whose body returns a typed local straight through, a bare tagged param ((fn [^int x] x)) or a let/loop binding of one, now declares that PHP return type on the emitted signature (nullable, union, and class types included) and propagates it to callers, giving opcache's tracing JIT more to specialize on. Purely additive: bare-literal bodies stay untyped, a loop binding rebound through value-promoting arithmetic (+', *') stays untyped so a BigInt result never trips a narrower return type, and no runtime behavior changes (#2718)
  • A let/loop binding whose init calls a Phel function with a primitive return :tag, a phel.core helper or a user defn, the tag either author-declared or inferred, now carries that type, so (let [s (make-greeting x)] (str s s)) lowers str over s to a native concat and (let [n (tally x)] (+ n 5)) to a native add, instead of runtime dispatch. Only the four primitive tags propagate (a nullable/union/class return never feeds a native scalar op), and a binding calling its own enclosing defn is skipped so a redefinition never reads a stale registry tag (#2712)

👥 Contributors

@Chemaclass @JesusValeraDev

Full Changelog: v0.47.0...v0.48.0

Downloads

v0.48.0


View release on GitHub