Releases: ponylang/ponyc
Release list
0.67.0
Fix crash when parsing, printing, or querying deeply nested JSON
Parsing, printing, or querying a deeply nested JSON value — for example a document of many thousands of nested arrays or objects — could overflow the stack and crash the program with a segfault, rather than returning a value or a catchable JsonParseError.
The json package now handles arbitrarily deep nesting, limited only by available memory.
Reject an out-of-range --ponygcinitial exponent
--ponygcinitial sets the heap size at which an actor first garbage collects, given as the exponent N in a 2^N byte threshold (the default is 2^14, or 16 KiB). Passing a value of 64 or greater on a 64-bit platform — for instance mistaking the flag for a byte count and passing --ponygcinitial 65536 — used to silently wrap around to a 1-byte threshold, making the actor garbage collect on nearly every allocation: the exact opposite of the large threshold the value implied. The runtime now rejects such a value with an error at startup instead of running with a wildly wrong threshold.
Restore the message send merging optimization
A compiler optimization that batches the garbage-collection bookkeeping for consecutive message sends had been silently disabled, so it never ran. It now runs again. Code that sends several messages in a row — especially bursts to the same actor — generates fewer garbage-collection messages at runtime, lowering overhead with no change to behavior.
Fix the start position reported for an empty JSON container's end token
JsonTokenParser reports a token_start() and token_end() byte offset for each token it emits. For the end token of an object or array — JsonTokenObjectEnd or JsonTokenArrayEnd — token_start() marks where the closing } or ] begins. For an empty container it used to report the position of the opening bracket instead, so token_start() .. token_end() spanned the whole {} or [] rather than just the closing bracket. It now reports the closing bracket, the same as a non-empty container.
Fix the start position reported for a JSON String or Key token
JsonTokenParser reports a token_start() and token_end() byte offset for each token it emits. For a String or Key token, token_start() now marks the opening ", so the reported span covers the whole quoted token. Previously it pointed one byte past the opening quote, so the span dropped the opening quote — inconsistent with every other token, which already reported its first byte.
Stop installing the Pony runtime's C headers and static libraries
On Linux, macOS, and the BSDs, make install used to place ponyc's C runtime headers (pony.h, paths.h, threads.h, and a few others) into <prefix>/include and its runtime static libraries (libponyrt.a and friends) into <prefix>/lib, where <prefix> defaults to /usr/local. These were only there to support embedding the Pony runtime in a non-Pony C program by hand, which Pony does not support.
They also caused a real problem. The installed paths.h shadowed the system's own <paths.h>, and threads.h shadowed the C11 <threads.h>, so an unrelated C program that included one of those system headers could pick up Pony's by mistake.
ponyc no longer installs those headers or libraries into the shared directories. If you have a from-source install from an earlier version, your next make install or make uninstall clears out the old symlinks — only its own symlinks, never a file you put there yourself. (A prior install done with a custom ponydir isn't recognized by the cleanup; remove its stale <prefix>/include/*.h and <prefix>/lib/libponyrt* symlinks by hand.)
Writing C shims for your Pony code is unaffected: ponyc hands its headers to the shim compiler directly, so #include <pony.h> and #include <ponyassert.h> (for pony_assert) keep working with no setup. The ponyc compiler, the pony-lsp/pony-lint/pony-doc tools, and linking against libponyc to use the compiler as a library are all unaffected.
Fix systematic testing being much slower than it should be
Programs run under systematic testing could run far slower than the amount of work warranted — slow enough, especially with a small number of scheduler threads, to look like they had hung. This has been fixed.
Fix runtime stats build options on Windows
The runtimestats and runtimestats_messages build options failed to compile on Windows, so you couldn't build the runtime with runtime statistics enabled there. Both options now build on Windows.
Fix pooltrack build option on Windows
The pooltrack build option failed to compile on Windows, so you couldn't build the runtime with pool allocation tracking enabled there. It now builds on Windows.
Fix delivery of zero-byte UDP datagrams
UDP sockets now deliver zero-byte datagrams to UDPNotify.received with an empty payload instead of treating them as an error and tearing the socket down. A zero-length datagram is valid per RFC 768, and applications that use them as heartbeats, keepalives, or presence pings will now receive them like any other datagram. TCP zero-byte read handling, where a zero-byte read means the peer closed, is unchanged.
Don't run the ASIO thread under systematic testing
Systematic testing (use=systematic_testing) replays a single scheduler interleaving from a seed so that a run is deterministic and reproducible. The ASIO thread — which handles sockets, standard input, process I/O, signals, and timers — is a real operating-system thread doing external work that a seed cannot control, so its presence left those runs only partly deterministic. A program that used any of that I/O under systematic testing would run anyway, carrying a source of nondeterminism the seed could not pin down.
Systematic testing no longer runs the ASIO thread. Any attempt to register an I/O event — opening a socket, reading standard input, spawning a process, installing a signal handler, or arming a timer — now aborts with a message saying I/O is not available under systematic testing. This is deliberate: rather than silently dropping the I/O and letting a program appear to be tested deterministically when it is not, the runtime says so plainly.
This removes the ASIO thread as a source of nondeterminism. It does not make every program deterministic on its own — a program can still introduce nondeterminism by other means, such as reading the wall clock through the FFI — and avoiding those remains the programmer's responsibility when a fully reproducible run is the goal.
Fix executables failing to start on Linux systems with both glibc and musl
On Linux systems that have both the GNU (glibc) and musl C libraries installed, ponyc could build a program that was linked against one C library but set up to load the other at startup. Compilation succeeded, but the program failed the moment you ran it, printing messages such as Error loading shared library or symbol not found.
ponyc now selects the startup loader that matches the C library your program is linked against. If you pass an explicit --triple, ponyc honors your choice instead of guessing from what is installed on the build machine.
Fix json package emitting invalid JSON for non-finite floating-point values
The json package serialized a non-finite floating-point number — infinity or NaN — as the bare word inf, -inf, nan, or -nan. None of those are valid JSON, so the output could not be parsed back, including by the package's own parser. A non-finite value now serializes as null.
let inf = F64(1e308) * F64(10)
JsonPrinter.print(JsonObject.update("v", inf))
// before: {"v":inf} (not valid JSON)
// after: {"v":null}Relatedly, parsing a numeric literal outside F64 range — such as 1e999, or an integer of several hundred digits — used to succeed, producing one of these non-finite values. Such a literal is now rejected as a parse error.
JsonParser.parse("1e999")
// before: a non-finite F64 that cannot be serialized back to valid JSON
// after: a JsonParseError, "Number out of range"Number parsing is also more accurate at the extremes: a literal that is mathematically zero but written with a large exponent (0e309) previously parsed to NaN, and some in-range literals with very long digit runs parsed to infinity or a slightly wrong value — these now parse correctly.
Add --version and --help support to pony-lsp
pony-lsp --version used to print nothing and hang: it ignored the flag, started the language server, and waited for input that never came. pony-lsp now handles --version and --help and exits, the same as ponyc, pony-doc, and pony-lint.
pony-lsp now rejects unrecognized command-line arguments
pony-lsp used to ignore everything on its command line and start the language server regardless. It now parses its arguments the same way ponyc, pony-doc, and pony-lint do — it recognizes --version and --help — and rejects anything else instead of ignoring it.
This means an editor configured to launch pony-lsp with an argument will now fail to start the server. The Helix configuration previously published on the Pony website passed a single empty-string argument:
[language-server.pony-lsp]
command = "pony-lsp"
args = [""]Remove the argument so args is an empty list:
[language-server.pony-lsp]
command = "pony-lsp"
args = []Fix Sig.usr2() reporting SIGUSR2 availability backwards
Sig.usr2() in the signals package had its availability reversed with respect to how the Pony runtime uses SIGUSR2, so it was unusable in every configuration.
On the default Linux and BSD builds, the runtime reserves SIGUSR2 for its own scheduler use, so a handler registered for it never fires — yet Sig.usr2() compiled and returned a signal number, silently handing you a handler that could never run. On macOS (and other `scheduler_scaling_pthrea...
0.66.0
Fix systematic testing build failure under gcc
Building with use=systematic_testing under gcc failed to compile with a spurious compiler error. The build now compiles cleanly under gcc.
Fix an intermittent systematic testing hang
Programs run under systematic testing could intermittently hang instead of running to completion. This has been fixed.
Replace Windows IOCP socket I/O with readiness notifications
Windows networking no longer uses I/O completion ports (IOCP). The runtime now performs socket I/O with readiness notifications via the Winsock ProcessSocketNotifications API — the same model the Linux (epoll) and macOS/BSD (kqueue) backends already use. If you run networked Pony on Windows, the entire socket I/O path beneath TCPConnection, TCPListener, and UDPSocket has changed, even though your own code does not need to.
Everything else below follows from this change: real TCP write backpressure, surfaced UDP send errors, muted-connection close detection that matches the other platforms, and a new Windows version floor. It also closes a memory-safety hole. Under the old completion-based model the kernel wrote into Pony buffers after the call that started the operation returned, which no reference capability could describe; reads and writes are now synchronous, so nothing the kernel touches escapes a capability.
Drop support for Windows 10
Because ProcessSocketNotifications exists only on Windows 11 and Windows Server 2022 (build 20348) and later, Windows 10 is no longer supported, and there is no fallback: a binary built with this release will fail to load on Windows 10. The supported Windows floor is now Windows 11 / Windows Server 2022.
Windows TCP write backpressure now reflects real socket writability
On Windows, TCPConnection previously decided when to apply write backpressure using a heuristic based on the number of writes in flight, rather than the actual state of the socket. throttled and unthrottled now fire based on real socket writability as reported by the operating system, the same as on Linux, macOS, and BSD.
Windows UDP send failures are no longer silently discarded
On Windows, a failed UDPSocket send previously produced no error and no notification at all. A send that fails now surfaces the error and closes the socket, delivering closed() to the UDPNotify, matching the behavior on other platforms. (A send that would merely block on a non-blocking socket is still silently dropped, as it is on every platform.)
Windows muted TCPConnection no longer detects peer close until unmuted
On Windows, a muted TCPConnection previously still noticed when its peer closed the connection. It now behaves like every other platform: while muted, a connection does not learn that its peer has closed until it is unmuted. As on all platforms, you must call unmute on a muted connection for it to close — without it the TCPConnection actor will never exit.
Improve memory usage
A program could use a large and growing amount of memory when an actor repeatedly forwarded an object it had received from another actor back to itself. Such programs now run in bounded memory.
Remove support for Alpine 3.21
We no longer test against it or build ponyc releases for it.
Remove support for Alpine 3.22
We no longer test against it or build ponyc releases for it.
Remove support for Ubuntu 22.04
We no longer test against it or build ponyc releases for it.
Update Docker image base to Alpine 3.24
The ponylang/ponyc:nightly and ponylang/ponyc:release Docker images now use Alpine 3.24 as their base image, updated from Alpine 3.23.
[0.66.0] - 2026-06-29
Fixed
- Fix systematic testing build failure under gcc (PR #5584)
- Fix an intermittent systematic testing hang (PR #5585)
- Improve memory usage (PR #5594)
Changed
0.65.0
Fix use=dtrace builds on FreeBSD
Building ponyc with use=dtrace failed on FreeBSD: the runtime build aborted with dtrace: failed to link script ... No probe sites found for declared provider, and even past that, programs compiled by a dtrace-enabled ponyc could not be linked.
use=dtrace now builds and links correctly on FreeBSD, and dynamically-linked programs expose their pony provider probes to DTrace.
--static programs build and run but do not expose their probes: FreeBSD registers DTrace USDT probes through the runtime linker, which statically-linked programs don't use.
Use embedded LLD for native Linux sanitizer builds
When ponyc is built with sanitizers (such as address_sanitizer or undefined_behavior_sanitizer) on Linux, it now links the programs it compiles with its built-in LLD linker, the same as every other build, instead of falling back to your system C compiler to perform the link. Sanitizer-enabled native Linux compilation no longer depends on having an external compiler driver present and usable as a linker.
Fix compiler crashes involving control expressions that jump away
A control expression that "jumps away" with no value — error, return,
break, or continue — has no type. Using one in several positions crashed the
compiler instead of compiling or reporting a clear error.
A repeat loop whose else clause jumps away now compiles correctly:
actor Main
new create(env: Env) =>
try let x: U8 = repeat 1 until false else error end endUsing a jump-away expression where a value is required now produces a clear
compile error instead of crashing the compiler. This covers many positions,
including conditions, match operands and guards, recover operands, call and
FFI arguments, method receivers, as and identity (is/isnt) operands,
default arguments, lambda captures, and tuple elements:
// each of these now reports an error rather than crashing the compiler
if error then U8(1) else U8(2) end
match error | let y: U8 => y else U8(0) end
recover error end
let n: U8 = some_function(error)
(error).string()
let t: (U8, U8) = (1, (error))Reject self-referential type parameter constraints
A generic type parameter whose constraint referred back to itself used to crash the compiler. For example:
class A[B: (B | C)]The compiler now reports a clear error for these constraints instead of crashing.
Report an error for infinitely recursive generic types
Some generic code instantiates itself with an ever-growing type argument, requiring an unbounded number of concrete types. For example, a function that calls itself with a deeper type on each step:
primitive Bar
fun apply[A: IFoo](n: USize): IFoo =>
if n == 0 then
A
else
Bar.apply[Pair[A]](n - 1)
endPreviously the compiler tried to generate every one of those types and kept going until it exhausted all available memory and crashed, with no indication of what in your code caused it. The compiler now stops once a generic instantiation grows past a fixed limit and reports an error pointing at the generic function or type responsible, so you get a clear diagnostic instead of an out-of-memory crash. Genuinely recursive generic types like this remain unsupported — Pony has to know every concrete type ahead of time — but the failure is now explained rather than silent.
Fix compiler crash on partial application of a method with a literal default argument
Partially applying a method whose default argument is built from a numeric literal would crash the compiler. For example, this crashed during compilation:
use "format"
actor Main
new create(env: Env) =>
Format~apply()Format.apply has a parameter whose default argument is -1, and partially applying the method triggered the crash. Any method with a comparable default (for instance 0 + 1) was affected. These now compile and behave correctly.
Relatedly, partially applying a method whose default argument is itself invalid — such as (-1).abs(), where the literal has no type to look up abs on — now reports a normal compile error instead of crashing the compiler.
Add JsonPrinter for serializing any JsonValue
The json package can now serialize any JsonValue — objects, arrays, and scalars alike — to a JSON string via the new JsonPrinter primitive. It is the dual of JsonParser: where JsonParser.parse turns a String into a JsonValue, JsonPrinter.print turns a JsonValue back into JSON.
Previously only JsonObject and JsonArray could be serialized; scalar values had no correct serializer (printing None produced None instead of null, and strings were not escaped). JsonPrinter handles the whole JsonValue union, so this is also the answer to "how do I serialize my data as JSON?": build a JsonValue, then hand it to JsonPrinter.
let doc = JsonObject
.update("name", "Alice")
.update("age", I64(30))
JsonPrinter.print(doc) // {"name":"Alice","age":30}
JsonPrinter.pretty(doc) // pretty-printed, two-space indent by default
JsonPrinter.print(None) // null
JsonPrinter.print("hi\"there") // "hi\"there"Rename JsonObject and JsonArray serialization methods
JsonObject and JsonArray no longer implement Stringable. Their string() and pretty_string() methods have been renamed to print() and pretty_print(), matching the new JsonPrinter and the parse/print naming in the package.
This is a breaking change. Code that serialized a JsonObject or JsonArray needs to call the new method names, and code that relied on these types being Stringable (for example passing one where a Stringable is expected) should use JsonPrinter.print instead.
// Before
let s = my_object.string()
let p = my_array.pretty_string()
// After
let s = my_object.print()
let p = my_array.pretty_print()
// or, for any JsonValue including scalars:
let s' = JsonPrinter.print(my_value)Fix compiler crashes in while and repeat loops that jump away
Several while and repeat loops whose body and/or else clause jump away crashed the compiler or, with a debug build, produced invalid code, instead of compiling or reporting a clear error. For example, all of these were broken:
actor Main
new create(env: Env) =>
// (1) jumps-away loop with an uninferable literal else
try repeat error until false else 2 end end
// (2) jumps-away loop with a value else whose result is used
let x: U8 = try repeat error until false else U8(2) end else U8(0) end
// (3) a break with a value while the else jumps away (while and repeat)
try repeat break U8(3) until false else error end end
try while true do break U8(3) else error end endThis is fixed:
- A
breakthat carries a value gives its loop both a value and an exit, so the loop no longer crashes when itselseclause jumps away — it compiles and yields the break value (case 3). The same is true of acontinuethat reaches a value-producingelse. This applies to bothwhileandrepeat. - A loop that genuinely jumps away (its body always errors or returns) now compiles correctly whether or not its result is used, including inside a
try(case 2). It simply produces no value. - A bare, uninferable literal in such a loop (in the
elseclause, or as abreakvalue with nothing to anchor it) is now rejected with a "could not infer literal type" error instead of crashing the compiler (case 1).
One related change: a while or repeat loop that jumps away makes any code after it unreachable, so the compiler now reports unreachable code for it — the same error an equivalent if already gives. This affects a loop used as the body of a function with no explicit return, such as while true do break else return end, which previously compiled.
Use embedded LLD for native FreeBSD sanitizer builds
When ponyc is built with sanitizers (such as address_sanitizer or undefined_behavior_sanitizer) on FreeBSD, it now links the programs it compiles with its built-in LLD linker, the same as every other build, instead of falling back to your system C compiler to perform the link. Sanitizer-enabled native FreeBSD compilation no longer depends on having an external compiler driver present and usable as a linker.
Reject tuple types hidden in an intersection within a type constraint
Tuple types can't be used as generic type constraints. The compiler already rejected a tuple smuggled into a constraint through a type alias, including when it was hidden inside a union. It did not, however, catch a tuple hidden inside an intersection, so the following incorrectly compiled:
type R is (U8 & (U8, U32))
class Block[T: R]The compiler now rejects this with the same error it already gives for tuples in unions:
constraint contains a tuple; tuple types can't be used as type constraints
Use embedded LLD for FreeBSD use=dtrace builds
When ponyc is built with use=dtrace on FreeBSD, it now links the programs it compiles with its built-in LLD linker, the same as every other build, instead of falling back to your system C compiler to perform the link. A use=dtrace compiler no longer depends on having an external compiler driver present and usable as a linker, and the programs it builds register and fire their pony provider DTrace probes exactly as before.
Update supported OpenBSD version to 7.9
The supported version of OpenBSD is now 7.9. Our continuous integration builds and tests ponyc against OpenBSD 7.9.
We won't intentionally break OpenBSD 7.8, but we are no longer actively maintaining support for it. If you need ponyc on OpenBSD, we recommend running 7.9.
Use embedded LLD for native macOS sanitizer builds
Native macOS sanitizer builds (use=address_sanitizer, use=undefined_behavior_sanitizer) now link through the embedded ld64.lld linker instead of requiring an exte...
0.64.0
Add LSP call hierarchy support
The Pony language server now supports the LSP call hierarchy protocol:
textDocument/prepareCallHierarchy— when the cursor is on a method or constructor, returns aCallHierarchyItemdescribing it.callHierarchy/incomingCalls— returns items for all methods in the workspace that call the given method, with the specific call sites within each caller.callHierarchy/outgoingCalls— returns items for all methods called by the given method, with the specific call sites within it.
Editors that support this protocol (VS Code, Neovim, etc.) expose it as "Show Call Hierarchy", "Show Incoming Calls", and "Show Outgoing Calls" commands.
Fix LSP range end positions overshooting past source line ends
Go-to-definition, document symbols, workspace symbols, type hierarchy, call hierarchy, and selection ranges could highlight text past the end of the declaration line. Editors that rely on this range for highlighting or cursor placement would overshoot into whitespace or the next token. This is now fixed.
Fix LSP hover showing on declaration keywords
Hovering over a declaration keyword (class, actor, trait, interface, primitive, type, struct, fun, be, new, let, var, or embed) incorrectly showed a hover popup for the entity, method, or field being declared. Hover information now only appears when hovering over the declaration name itself.
Fix LSP hover showing on capability keywords
Hovering over a capability keyword (iso, trn, ref, val, box, tag) no longer shows hover information. Previously, hovering on the receiver cap in fun ref foo() or the type cap in String val would pop up method or type hover info, which was incorrect.
Fix compiler hang and crash on recursive generic types
Previously, the compiler would hang or crash on two shapes of recursive generic type. Both now compile or produce a normal type error.
A recursive generic interface whose method return type references the same interface with strictly larger type arguments would hang the compiler indefinitely:
// Drifting via tuple typeargs (ponylang/ponyc#1216).
interface Iter[A]
fun enum[B](): Iter[(B, A)] => thisA type parameter whose constraint references the parameter itself would crash the compiler with a stack overflow:
// Recursive type parameter constraint (ponylang/ponyc#3930).
fun flatten[A: Array[Array[A]] #read](arrayin: Array[Array[A]]): Array[A] =>
...The root cause was that exact_nominal in the structural subtype checker compared typeargs via is_eq_typeargs, which calls back into the subtype machinery and re-enters check_assume on the same recursive shapes. Replacing that with a structural AST equality check that compares definition pointers directly — without re-entering the subtype check — eliminates the re-entry while preserving semantic identity (two type parameters that share a source name in different scopes are correctly distinguished). Red Davies originally authored a fix along these lines for #3930. Combined with the new SAME_DEF_LIMIT divergence guard in is_nominal_sub_nominal, which bounds the depth of any single drifting recursion chain, the compiler now terminates on both shapes above.
LSP: drop behaviour return type from hover and signature help
Hover popups and signature help for be behaviours no longer display a return type. Behaviour return types are always None val inserted by the compiler and cannot be written explicitly in source, so showing them was unnecessary and did not add information.
Fix LSP parameter hover to show valid Pony syntax
Hovering over a method parameter in the LSP previously showed param name: String — a param keyword that does not exist in Pony. It now shows name: String, which is the correct representation of a parameter.
LSP now shows docstrings for class fields on hover
Docstrings on class fields are now shown when you hover over a field in your editor, consistent with how docstrings on classes, actors, primitives, and methods are already displayed.
Fix Windows TCP connection silently leaking state on IOCP errors
When a Windows TCP connection's underlying socket failed during an IOCP write, or when the IOCP completion bookkeeping became inconsistent, the connection would silently leak its pending write state instead of closing. Subsequent writes would accumulate on a dead socket.
The connection now closes non-gracefully when these errors occur, matching the POSIX behavior.
Fix LSP hover for lambda types
Hovering over a field, parameter, or variable with a lambda type annotation now shows the human-readable lambda type instead of a compiler-internal hygienic ID.
Before:
let _callback: $0 valAfter:
let _callback: {(String val): None val} valTrim whitespace from INI section names
Previously, the INI parser left whitespace inside [ name ] as part of the section name, so an input like:
[ section ]
key = valueproduced a section named " section ". Looking it up as "section" missed.
The parser already trims whitespace from lines, keys, and values. Not trimming section names was inconsistent rather than a deliberate dialect choice.
Section names are now trimmed of leading and trailing whitespace. [ name ] parses as name. Internal whitespace is preserved — [a b] is still "a b". [] and [ ] are both the empty-string section.
This is a behavior change: any existing INI input that relied on the old quirk to distinguish sections by surrounding whitespace (e.g., treating [section] and [ section ] as different sections) will now see those sections collapse into one. Because IniParse overwrites duplicate keys with the last value seen, keys from the earlier section can be silently overwritten.
Fix spurious LSP inlay hints inside lambda type annotations
Nominal types appearing inside lambda type annotations (e.g. {(String): None} val or {(): String} val) were causing spurious capability inlay hints at incorrect source positions — for example, a val hint would appear in the middle of a type name. These hints no longer appear.
Fix LSP document symbol outline dropping class members after lambda field initialisers
If a class contained a let field whose initialiser was a lambda literal (e.g. let _f: {(U32): U32} val = {(x: U32): U32 => x}), all class members declared after that field were silently missing from the document symbol outline (the structure view shown by editors).
The outline now correctly includes all named class members regardless of whether the class contains lambda field initialisers, lambda-typed fields, methods returning lambdas, or methods with lambda-typed parameters.
Fix compiler crash when combining iftype and as
Previously, applying as to the result of an iftype expression that contained a method call on a narrowed type parameter would crash the compiler with an internal assertion failure (#2042):
class LitString
interface AST
interface HasDocs
fun val docs(): (LitString | None)
actor Main
new create(env: Env) => None
fun foo[A: AST val](node: A) =>
try
iftype A <: HasDocs
then node.docs()
end as LitString
endThe compiler now compiles this code correctly.
Fix missing question mark check for partial calls in trait default bodies
Calling a partial method without ? inside a trait's default method body now correctly produces a compile error, matching the behavior of primitives and interfaces.
Previously, code like this compiled silently:
trait T
fun f1() ? => error
fun f2() ? => f1() // missing `?`, but compiler did not complainThe same code in a primitive or interface correctly errored; only traits were affected.
If your code was relying on this missing check, add the ? to the call site:
trait T
fun f1() ? => error
fun f2() ? => f1()?Fix typecheck assertion failure on loops whose branches all jump away
Previously, defining a loop whose body and else clause both jump away (for example, a while with break in the body and return in the else) inside a function other than create triggered an internal compiler assertion:
actor Main
fun a() =>
while true do
break
else
return
end
new create(env: Env) =>
a()src/libponyc/pass/expr.c:698: pass_expr: Assertion `errors_get_count(options->check.errors) > 0` failed.
This has been fixed. Loops whose branches all jump away now compile correctly.
Fix compiler crash when a behavior satisfies a non-tag interface method
Previously, the compiler would crash with an assertion failure when an actor's behavior was used to satisfy an interface method declared with a box or ref receiver capability:
interface IFunBox
fun box apply(s: String)
actor Main
let _env: Env
new create(env: Env) =>
_env = env
let x: IFunBox = this
x("hello")
be apply(s: String) => _env.out.print(s)This is a valid subtype relationship — a behavior runs with a tag receiver, and box/ref are both subcaps of tag, so the contravariant receiver check holds. The crash has been fixed. Code of this shape now compiles and runs correctly.
Allow finite recursive type aliases
Type aliases can now reference themselves, as long as the resulting type has a finite layout. The compiler used to reject every self-referential alias, including ones that would have been perfectly fine to construct — JSON-like data, parse trees, and other tree-shaped patterns couldn't be expressed as aliases.
use "collections"
// Legal: JSON-like recursive structure.
type JsonValue is
( String
| F64
| Bool
| None
| Array[JsonValue]
| Map[String, JsonValue])
// Legal: tree built from a generic carrier.
type Tree is (None | Array[Tree])The aliases declare type shape. As with a...
0.63.4
Ubuntu 26.04 added as a supported platform
We've added arm64 and amd64 builds for Ubuntu 26.04. We'll be building ponyc releases for it until it stops receiving security updates in 2031. At that point, we'll stop building releases for it.
Add LSP textDocument/signatureHelp support
The Pony language server now supports textDocument/signatureHelp, providing parameter hints when the cursor is inside a call expression. The popup shows the full method signature with the active parameter highlighted, and includes the method's docstring when present.
Triggered by ( and , characters, consistent with standard LSP conventions.
Signature help is driven by the compiled AST and requires the file to be saved — it is not available mid-keystroke while the file has unsaved edits.
Fix linking failures on Fedora and other RPM-based distributions
Starting with ponyc 0.61.1, attempting to link a Pony program on Fedora (and other RPM-based distributions such as RHEL, CentOS, Rocky, and openSUSE) failed with:
Error:
could not find libc CRT objects in sysroot ''
ponyc now locates the C runtime startup objects on these distributions, and linking succeeds.
Remove binutils-gold from nightly and release Docker images
The binutils-gold package has been removed from the ponylang/ponyc nightly and release Docker images. Nothing in ponyc uses the gold linker, and gold is upstream-deprecated and being phased out of distro repositories.
If your build inside one of these images relies on binutils-gold being present, you will need to install it explicitly with apk add binutils-gold.
Reject wrong-architecture libc startup objects on multilib hosts
Compiling a Pony program on a multilib Linux host could fail with a confusing arch-mismatch error from the embedded linker. For example, on an x86_64 Fedora system with glibc-devel.i686 installed, ponyc would pick up the 32-bit /usr/lib/crt1.o and the link would fail with:
ld.lld: error: /usr/lib/crt1.o is incompatible with elf_x86_64
ponyc now validates the architecture of each candidate crt1.o and skips ones that don't match the target. If no matching crt1.o is found, the error message names the target architecture instead of falling through to the linker's lower-level error.
Add pony-lsp inlay hints for function parameter types
pony-lsp now shows capability hints on function parameter type annotations where the capability is omitted from source.
-fun box greet(name: String, items: Array[String]): None val =>
+fun box greet(name: String val, items: Array[String val] ref): None val =>
NoneHints appear after each type name (and after ] for generic types). Parameters with explicit capabilities are unaffected — no hint is shown when the capability is already written out.
Type parameter references (e.g. T in Array[T]) have no fixed capability, so they produce no hint.
Fix spurious pony-lsp inlay hints on primitive types
Primitive types were showing extra unexpected inlay hints alongside the hints for user-defined methods. This is now fixed.
Fix match exhaustiveness for Bool value patterns in tuples
Previously, matching on a Bool inside a tuple required an else clause even when both true and false were covered:
primitive Foo
fun apply(x: (String, Bool)): Bool =>
match x
| (_, true) => true
| (_, false) => false
endError:
main.pony:3:5: function body isn't the result type
This has been fixed. Bool value patterns inside tuples now participate in exhaustiveness checking, so (_, true) and (_, false) correctly cover (String, Bool). This also works with nested tuples, multiple Bool elements, and Bool type aliases.
Fix pony-lsp document symbols showing spurious eq/ne for bare primitives
The document symbol outline (textDocument/documentSymbol) incorrectly included eq and ne entries for bare primitives that appeared last in their file. These methods are synthesized by the compiler and should not appear in the outline. They now correctly have no children.
Add LSP type hierarchy support
The Pony language server now supports the LSP type hierarchy protocol:
textDocument/prepareTypeHierarchy— places a cursor on a class, trait, actor, interface, primitive, or struct and returns aTypeHierarchyItemdescribing it.typeHierarchy/supertypes— returns items for each type in the entity'sis(provides) list.typeHierarchy/subtypes— cross-package walk that returns items for every entity whoseislist directly includes the given type.
Editors that support this protocol (VS Code, Neovim, etc.) expose it as "Show Type Hierarchy", "Go to Supertypes", and "Go to Subtypes" commands.
[0.63.4] - 2026-05-02
Fixed
- Fix linking failures on Fedora and other RPM-based distributions (PR #5262)
- Reject wrong-architecture libc startup objects on multilib hosts (PR #5271)
- Fix spurious pony-lsp inlay hints on primitive types (PR #5275)
- Fix match exhaustiveness for Bool value patterns in tuples (PR #5226)
- Fix pony-lsp document symbols showing spurious eq/ne for bare primitives (PR #5278)
Added
- Ubuntu 26.04 added as a supported platform (PR #5256)
- Add LSP
textDocument/signatureHelpsupport (PR #5259) - Add pony-lsp inlay hints for function parameter types (PR #5274)
- Add LSP type hierarchy support (PR #5297)
Changed
- Remove binutils-gold from nightly and release Docker images (PR #5270)
0.63.3
Strengthen memory ordering on runtime queue pushes
Pony's runtime uses several concurrent queues to hand off messages and actors between threads. On x86, atomic read-modify-writes are always full memory barriers regardless of the memory ordering requested by the program, so the previous code behaved correctly there. On ARM, aarch64, and other weakly-ordered architectures, the previous code relied on a subtle C11 memory-model rule (release-sequence extension across cross-thread read-modify-writes) to establish the required happens-before between one scheduler thread releasing an actor or a message and another scheduler thread picking it up.
This rule is correct under C11 but was narrowed in C++20, and depending on it made the runtime fragile under evolving compiler interpretations. It is a candidate contributor to the rare, recurring aarch64 stress-test crashes tracked in #5243 (originally reported as #4069).
The push operations on the actor message queue and on the scheduler's multi-producer multi-consumer queue have been changed to establish happens-before directly with acquire-release read-modify-writes instead of through the release-sequence rule. The multi-producer push paths generate identical machine code on x86 because x86's atomic read-modify-writes are always full barriers regardless of the requested ordering. The single-producer push paths replace two plain atomic stores with one xchg on x86. On ARM and other weakly-ordered platforms, the generated code changes in all paths but shouldn't have an impact on performance. Stronger memory ordering should not introduce any incorrect behavior; the change is strictly defensive.
Add LSP textDocument/selectionRange support
The Pony language server now handles textDocument/selectionRange requests, enabling editors to expand the selection to progressively larger syntactic units (e.g. Alt+Shift+→ in VS Code).
For a given cursor position the server returns a chain of nested ranges — innermost first — walking up the AST from the token under the cursor through its enclosing expressions, function, class body, and finally the whole file. Ancestor nodes whose span is identical to their child are collapsed so that each step in the chain produces a visible selection change. Descendant nodes from other source files (such as trait methods merged into a class by the compiler) are excluded so that the ranges always stay within the current file.
Add LSP workspace/symbol support
The Pony language server now handles workspace/symbol requests, enabling workspace-wide symbol search (e.g. "Go to Symbol in Workspace" / Cmd+T in VS Code).
Given a query string, the server performs a case-insensitive substring search over all compiled symbols — top-level types (class, actor, struct, primitive, trait, interface) and their members (constructors, functions, behaviours, fields) — across every package in the workspace. An empty query returns all symbols. Results are returned as a flat SymbolInformation[] with file URI and source range; member symbols include a containerName identifying the enclosing type.
The server advertises workspaceSymbolProvider: true in its capabilities.
Fix compiler crash when passing an array literal to OutStream.writev
Previously, the compiler would crash with an assertion failure when an array literal was passed to env.out.writev (or any other call expecting a ByteSeqIter):
actor Main
new create(env: Env) =>
env.out.writev([])
env.out.writev(["foo"; "bar"])The compiler's element-type inference for array literals, when the antecedent was an interface whose values method returned a type alias (such as ByteSeqIter, where ByteSeq is (String | Array[U8] val)), failed to fully strip viewpoint arrows from the inferred type. The leftover arrows eventually reached code generation and triggered an internal assertion.
This has been fixed. Code of the shape above now compiles and runs correctly.
Fix runtime crash with iso in mixed-capability union type
Matching on a destructively read iso field would crash at runtime with a segfault in the GC when the field's union type also contained a val member. For example, this program would crash:
class A
var v: Any val
new create(v': Any val) =>
v = consume v'
class B
actor Foo
var _x: (A iso | B val | None)
new create(x': (A iso | B val | None)) =>
_x = consume x'
be f() =>
match (_x = None)
| let a: A iso => None
end
actor Main
new create(env: Env) =>
let f = Foo(recover A("hello".string()) end)
f.f()The crash occurred because the GC traced the iso object incorrectly, leading to a reference count imbalance and use-after-free. This has been fixed.
Fix LSP symbol ranges
LSP clients that use textDocument/documentSymbol (the outline/breadcrumb view in most editors) could produce a "selectionRange must be contained in range" error, causing the entire symbol list to be rejected. This is now fixed.
In addition, symbol ranges across both textDocument/documentSymbol and workspace/symbol now correctly cover the full declaration — from the opening keyword to the end of the body. Previously, textDocument/documentSymbol ranges covered only the declaration keyword (class, fun, etc.), and workspace/symbol ranges covered only the identifier. Highlighting a symbol or jumping to it now selects the whole declaration.
Fix LSP definition and type-definition ranges
textDocument/definition and textDocument/typeDefinition responses now return a range that covers the full declaration — from the opening keyword to the end of the body. Previously the range covered only the declaration keyword (class, fun, etc.).
Range computation now also correctly handles the last type declaration in a file. Previously the compiler's synthesized default constructors could cause the final entity's range to extend to an incorrect position; this no longer occurs.
Fix LSP outline including synthetic and inherited members
Previously, the textDocument/documentSymbol response (used by editors to build the outline/symbol tree) included members that were not explicitly written in a class's source file. A bare class that inherited a trait with a default method, or any class without an explicit constructor, would have those synthesized or inherited members appear as children in the outline.
This has been fixed. The outline now shows only members that are explicitly written in the file being viewed.
[0.63.3] - 2026-04-25
Fixed
- Fix compiler crash when passing an array literal to
OutStream.writev(PR #5192) - Fix runtime crash with iso in mixed-capability union type (PR #4809)
- Fix LSP symbol range semantics (PR #5241)
- Fix LSP definition and type-definition ranges (PR #5247)
- Fix LSP outline including synthetic and inherited members (PR #5249)
Added
Changed
- Strengthen memory ordering on runtime queue pushes (PR #5245)
0.63.2
Add LSP workspace/inlayHint/refresh support
After each compilation, pony-lsp now sends a workspace/inlayHint/refresh request to the editor, asking it to re-request inlay hints for all open documents. Previously, inlay hints (such as inferred type annotations) would not update after a file was saved and recompiled. This only takes effect when the editor advertises support for workspace/inlayHint/refresh in its LSP capabilities (all major editors do).
Extend LSP inlay hints with generic caps, return type caps, and receiver caps
The pony-lsp inlay hint feature now covers additional implicit capability annotations:
- Generic type annotations: capability hints on type arguments in generic types, including nested generics, union/intersection/tuple members, class fields (
let,var,embed), and function return types. - Receiver capability: a hint after
funshowing the implicit capability (e.g.box) when no explicit cap keyword is written. Not emitted forbeornew. - Return type capability: when a function has an explicit return type annotation, a capability hint is added after the type name if the cap is absent.
- Inferred return type: when a function has no return type annotation, a hint shows the full inferred return type (e.g.
: None val).
Add LSP textDocument/declaration support
The Pony language server now handles textDocument/declaration requests. In Pony there are no separate declaration sites — declaration and definition are always the same location — so the handler routes directly to the existing go-to-definition implementation. The server advertises declarationProvider: true in its capabilities.
Add LSP textDocument/rename and textDocument/prepareRename support
The Pony language server now supports symbol rename. Placing the cursor on any renameable identifier — field, method, behaviour, local variable, parameter, type parameter, class, actor, struct, primitive, trait, or interface — and invoking Rename Symbol in your editor will produce a WorkspaceEdit replacing every occurrence across all packages in the workspace.
textDocument/prepareRename is also implemented, allowing editors to validate that the cursor is on a renameable symbol before prompting for the new name. The server advertises prepareProvider: true in its capabilities.
Renames are rejected with an appropriate error when:
- The cursor is on a literal or synthetic expression node.
- The target symbol is defined outside the workspace (stdlib or external package).
- The supplied new name is not a valid Pony identifier.
Add LSP textDocument/typeDefinition support
The Pony language server now supports Go to Type Definition. Placing the cursor on any symbol with a known type — a local variable, parameter, or field — and invoking Go to Type Definition in your editor will navigate to the declaration of the symbol's type rather than the symbol itself.
This works for explicitly annotated bindings (let x: MyClass) and for bindings whose type is inferred (let x = MyClass.create()).
Add LSP textDocument/foldingRange support
The Pony language server now handles textDocument/foldingRange requests, enabling editors to show fold regions for Pony source files.
A fold range is emitted for each top-level type entity (class, actor, struct, primitive, trait, interface) and for each multi-line member (fun, be, new). Within method bodies, fold ranges are also emitted for compound expressions: if (including ifdef, resolved to if by the compiler), while (including for, desugared to while by the compiler), repeat, match, try, and recover blocks. Single-line nodes are excluded since there is nothing to fold.
The server also sends workspace/foldingRange/refresh after each compilation when the editor advertises support for it, so that fold regions update automatically when files change.
[0.63.2] - 2026-04-19
Added
- Add LSP
workspace/inlayHint/refreshsupport (PR #5224) - Extend LSP inlay hints with generic caps, return type caps, and receiver caps (PR #5222)
- Add LSP
textDocument/declarationsupport (PR #5229) - Add LSP
textDocument/renameandtextDocument/prepareRenamesupport (PR #5228) - Add LSP
textDocument/typeDefinitionsupport (PR #5231)
0.63.1
Add style/docstring-leading-blank lint rule
pony-lint now flags docstrings where a blank line immediately follows the opening """. The first line of content should begin on the line right after the opening delimiter.
// Flagged — blank line after opening """
class Foo
"""
Foo docstring.
"""
// Clean — content starts on the next line
class Foo
"""
Foo docstring.
"""Types and methods annotated with \nodoc\ are exempt, consistent with style/docstring-format.
Fix rare silent connection hangs on macOS and BSD
On macOS and BSD, if the OS failed to fully register an I/O event (due to resource exhaustion or an FD race), the failure was silently ignored. Actors waiting for network events that were never registered would hang indefinitely with no error. This could appear as connections that never complete, listeners that stop accepting, or timers that stop firing — with no indication of what went wrong.
The runtime now detects these registration failures and notifies the affected actor, which tears down cleanly — the same as any other I/O failure. Stdlib consumers like TCPConnection and TCPListener handle this automatically.
If you implement AsioEventNotify outside the stdlib, you can now detect registration failures with the new AsioEvent.errored predicate. Without handling it, a failure is silently ignored (the same behavior as before, but now you have the option to detect it):
be _event_notify(event: AsioEventID, flags: U32, arg: U32) =>
if AsioEvent.errored(flags) then
// Registration failed — tear down
_close()
return
end
// ... normal event handlingFix rare silent connection hangs on Linux
On Linux, if the OS failed to register an I/O event (due to resource exhaustion or an FD race), the failure was silently ignored. Actors waiting for network events that were never registered would hang indefinitely with no error. This could appear as connections that never complete, listeners that stop accepting, or timers that stop firing — with no indication of what went wrong.
The runtime now detects these registration failures and notifies the affected actor, which tears down cleanly — the same as any other I/O failure. Stdlib consumers like TCPConnection and TCPListener handle this automatically.
Also fixes the ASIO backend init to correctly detect epoll_create1 and eventfd failures (previously checked for 0 instead of -1), and to clean up all resources on partial init failure.
LSP: Fix goto_definition range end
The Pony language server textDocument/definition response now returns a correct range.end position. Previously, it had an off-by-one error in the column value.
Add hierarchical configuration for pony-lint
pony-lint now supports .pony-lint.json files in subdirectories, not just at the project root. A subdirectory config overrides the root config for all files in that subtree, using the same JSON format.
For example, to turn off the style/package-docstring rule for everything under your examples/ directory, add an examples/.pony-lint.json:
{"rules": {"style/package-docstring": "off"}}Precedence follows proximity — the nearest directory with a setting wins. Category entries (e.g., "style": "off") override parent rule-specific entries in that category. Omitting a rule from a subdirectory config defers to the parent, not the default.
Malformed subdirectory configs produce a lint/config-error diagnostic and fall through to the parent config — the subtree is still linted, just with the parent's rules.
Protect pony-lint against oversize configuration files
pony-lint now rejects .pony-lint.json files larger than 64 KB. With hierarchical configuration, each directory in a project can have its own config file — an unexpectedly large file could cause excessive memory consumption. Config files that exceed the limit produce a lint/config-error diagnostic with the file size and path.
Protect pony-lint against oversize ignore files
pony-lint now rejects .gitignore and .ignore files larger than 64 KB. With hierarchical ignore loading, each directory in a project can have its own ignore files — an unexpectedly large file could cause excessive memory consumption. Ignore files that exceed the limit or that cannot be opened produce a lint/ignore-error diagnostic with exit code 2.
Add LSP textDocument/documentHighlight support
The Pony language server now handles textDocument/documentHighlight requests. Placing the cursor on any symbol highlights all occurrences in the file, covering fields, locals, parameters, constructors, functions, behaviours, and type names.
Fix pony-lsp failures with some code constructs
Fixed go-to-definition failing for type arguments inside generic type aliases. For example, go-to-definition on String or U32 in Map[String, U32] now correctly navigates to their definitions. Previously, these positions returned no result.
Fix silent timer hangs on Linux
On Linux, if a timer system call failed (due to resource exhaustion or other system error), the failure was silently ignored. Actors waiting for timer notifications would hang indefinitely with no error — timers that should fire simply never did.
The runtime now detects timer setup and arming failures and notifies the affected actor, which tears down cleanly — the same as any other I/O failure. Stdlib consumers like Timers handle this automatically.
Add LSP textDocument/inlayHint support
pony-lsp now supports inlay hints. Editors that request textDocument/inlayHint will receive inline type annotations after the variable name for let and var declarations whose type is inferred rather than explicitly written.
Add LSP textDocument/references support
The Pony language server now handles textDocument/references requests. References searches across all packages in the workspace, and supports the includeDeclaration option to optionally include the definition site in the results.
Fix pony-lsp hanging after shutdown and exit
pony-lsp would hang indefinitely after receiving the LSP shutdown request followed by the exit notification. The process had to be killed manually. The exit handler now properly disposes all actors, allowing the runtime to shut down cleanly.
Fix pony-lsp hanging on startup on Windows
pony-lsp was unresponsive on Windows when launched by an editor. The LSP base protocol uses explicit \r\n sequences in message headers, but Windows opens stdout in text mode by default, which translates every \n to \r\n. This turned the header separator \r\n\r\n into \r\r\n\r\r\n on the wire — a sequence that LSP clients don't recognize, causing them to wait forever for the end of the headers.
pony-lsp now sets stdout to binary mode on Windows at startup, so \r\n is written to the pipe unchanged.
Fix type checking failure for interfaces with interdependent type parameters
Previously, interfaces with multiple type parameters where one parameter appeared as a type argument to the same interface would fail to type check:
interface State[S, I, O]
fun val apply(state: S, input: I): (S, O)
fun val bind[O2](next: State[S, O, O2]): State[S, I, O2]Error:
type argument is outside its constraint
argument: O #any
constraint: O2 #any
The compiler replaced type variables one at a time during reification, so replacing S with its value could inadvertently transform a different parameter's constraint before that parameter was processed. This has been fixed by replacing all type variables in a single pass.
Fix incorrect code generation for this-> in lambda type parameters
When a lambda type used this-> for viewpoint adaptation (e.g., {(this->A)}), the compiler desugared it into an anonymous interface where this incorrectly referred to the interface's own receiver rather than the enclosing class's receiver. This caused wrong vtable dispatch, incorrect results, or segfaults when the lambda was forwarded to another function.
class Container[A: Any #read]
fun box apply(f: {(this->A)}) =>
f(_value)The desugaring now correctly preserves the polymorphic behavior of this-> across different receiver capabilities.
Add LSP go-to-definition for type aliases
The Pony language server now supports go-to-definition on type alias names. For example, placing the cursor on Map in Map[String, U32] and invoking go-to-definition navigates to the type Map declaration in the standard library. Previously, go-to-definition only worked on the type arguments (String, U32) but not on the alias name itself.
This also works for local type aliases defined in the same package.
Fix soundness hole in match capture bindings
Match let bindings with viewpoint-adapted or generic types could bypass the compiler's capability checks, allowing creation of multiple iso references to the same object. A direct let x: Foo iso capture was correctly rejected, but let x: this->B iso and let x: this->T (where T could be iso) slipped through because viewpoint adaptation through box erases the ephemeral marker that the existing check relies on to detect unsoundness.
The compiler now checks whether a capture type has a capability that would change under aliasing (iso, trn, or a generic cap that includes them) and rejects the capture when the match expression isn't ephemeral. Previously-accepted code that hits this check was unsound and could segfault at runtime.
How to fix code broken by this change
Consume the match expression so the discriminee is ephemeral:
Before (unsound, now rejected):
match u
| let ut: T =>
do_something(consume ut)
else
(consume u, default())
endAfter:
match consume u
| let ut: T =>
do_something(consume ut)
| let uu: U =>
(consume uu, default())
endThe else branch becomes | let uu: U => because u is consumed and no longer...
0.63.0
Fix use-after-free in IOCP ASIO system
We fixed a pair of use-after-free races in the Windows IOCP event system. A previous fix introduced a token mechanism to prevent IOCP callbacks from accessing freed events, but missed two windows where raw pointers could outlive the event they pointed to. One was between the callback and event destruction, the other between a queued message and event destruction.
This is the hard part that Pony protects you from. Concurrent access to mutable data across threads is genuinely difficult to get right, even when you have a mechanism designed specifically to handle it.
Remove support for Alpine 3.20
Alpine 3.20 has reached end-of-life. We no longer test against it or build ponyc releases for it.
Fix with tuple only processing first binding in build_with_dispose
When using a with block with a tuple pattern, only the first binding was processed for dispose-call generation and _ validation. Later bindings were silently skipped, which meant dispose was never called on them and _ in a later position was not rejected.
For example, the following code compiled without error even though _ is not allowed in a with block:
class D
new create() => None
fun dispose() => None
actor Main
new create(env: Env) =>
with (a, _) = (D.create(), D.create()) do
None
endThis now correctly produces an error: _ isn't allowed for a variable in a with block.
Additionally, valid tuple patterns like with (a, b) = (D.create(), D.create()) do ... end now correctly generate dispose calls for all bindings, not just the first.
Fix memory leak in Windows networking subsystem
Fixed a memory leak on Windows where an IOCP token's reference count was not decremented when a network send operation encountered backpressure. Over time, this could cause memory to grow unboundedly in programs with sustained network traffic.
Remove docgen pass
We've removed ponyc's built-in documentation generation pass. The --docs, -g, and --docs-public command-line flags no longer exist, and --pass docs is no longer a valid compilation limit.
Use pony-doc instead. It shipped in 0.61.0 as the replacement and has been the recommended tool since then. If you were using --docs-public, pony-doc generates public-only documentation by default. If you were using --docs to include private types, use pony-doc --include-private.
Fix spurious error when assigning to a field on an as cast in a try block
Assigning to a field on the result of an as expression inside a try block incorrectly produced an error about consumed identifiers:
class Wumpus
var hunger: USize = 0
actor Main
new create(env: Env) =>
let a: (Wumpus | None) = Wumpus
try
(a as Wumpus).hunger = 1
endcan't reassign to a consumed identifier in a try expression if there is a
partial call involved
The workaround was to use a match expression instead. This has been fixed. The as form now compiles correctly, including when chaining method calls before the field assignment (e.g., (a as Wumpus).some_method().hunger = 1).
Fix segfault when using Generator.map with PonyCheck shrinking
Using Generator.map to transform values from one type to another would segfault during shrinking when a property test failed. For example, this program would crash:
let gen = recover val
Generators.u32().map[String]({(n: U32): String^ => n.string()})
end
PonyCheck.for_all[String](gen, h)(
{(sample: String, ph: PropertyHelper) =>
ph.assert_true(sample.size() > 0)
})?The underlying compiler bug affected any code where a lambda appeared inside an object literal inside a generic method and was then passed to another generic method. The lambda's apply method was silently omitted from the vtable, causing a segfault when called at runtime.
Add --shuffle option to PonyTest
PonyTest now has a --shuffle option that randomizes the order tests are dispatched. This catches a class of bug that's invisible under fixed ordering: test B passes, but only because test A ran first and left behind some state. You won't find out until someone removes test A and something breaks in a way that's hard to trace.
Use --shuffle for a random seed or --shuffle=SEED with a specific U64 seed for reproducibility. When shuffle is active, the seed is printed before any test output:
Test seed: 8675309
Grab that seed from your CI log and pass it back to reproduce the exact ordering:
./my-tests --shuffle=8675309
Shuffle applies to all scheduling modes. For CI environments that run tests sequentially to avoid resource contention, --sequential --shuffle is the recommended combination: stable runs without flakiness, and each run uses a different seed so test coupling surfaces over time instead of hiding forever.
--list --shuffle=SEED shows the test names in the order that seed would produce, so you can preview orderings without running anything.
Fix pony-lint blank-lines rule false positives on multi-line docstrings
The style/blank-lines rule incorrectly counted blank lines inside multi-line docstrings as blank lines between members. A method or field whose docstring contained blank lines (e.g., between paragraphs) would be flagged for having too many blank lines before the next member. The rule now correctly identifies where a docstring ends rather than using only its start line.
Fix FloatingPoint.frexp returning unsigned exponent
FloatingPoint.frexp (and its implementations on F32 and F64) returned the exponent as U32 when C's frexp writes a signed int. Negative exponents were silently reinterpreted as large positive values.
The return type is now (A, I32) instead of (A, U32). If you destructure the result and type the exponent, update it:
// Before
(let mantissa, let exp: U32) = my_float.frexp()
// After
(let mantissa, let exp: I32) = my_float.frexp()Fix asymmetric NaN handling in F32/F64 min and max
F32.min and F64.min (and max) gave different results depending on which argument was NaN. F32.nan().min(5.0) returned 5.0, but F32(5.0).min(F32.nan()) returned NaN. The result of a min/max operation shouldn't depend on argument order.
The root cause was the conditional implementation if this < y then this else y end. IEEE 754 comparisons involving NaN always return false, so the else branch fires whenever this is NaN but not when only y is NaN.
Use LLVM intrinsics for NaN-propagating float min and max
Float min and max now use LLVM's llvm.minimum and llvm.maximum intrinsics instead of conditional comparisons. These implement IEEE 754-2019 semantics: if either operand is NaN, the result is NaN.
This is a breaking change. Code that relied on min/max to silently discard a NaN operand will now get NaN back. That said, the old behavior was order-dependent and unreliable, so anyone depending on it was already getting inconsistent results.
Before:
// Old behavior: result depended on argument order
F32.nan().min(F32(5.0)) // => 5.0
F32(5.0).min(F32.nan()) // => NaNAfter:
// New behavior: NaN propagates regardless of position
F32.nan().min(F32(5.0)) // => NaN
F32(5.0).min(F32.nan()) // => NaN[0.63.0] - 2026-04-04
Fixed
- Fix use-after-free in IOCP ASIO system (PR #5091)
- Fix with tuple only processing first binding in build_with_dispose (PR #5095)
- Fix memory leak in Windows networking subsystem (PR #5096)
- Fix spurious error when assigning to a field on an
ascast in a try block (PR #5070) - Fix segfault when using Generator.map with PonyCheck shrinking (PR #5006)
- Fix pony-lint blank-lines rule false positives on multi-line docstrings (PR #5109)
- Fix
FloatingPoint.frexpreturning unsigned exponent (PR #5113) - Fix asymmetric NaN handling in F32/F64 min and max (PR #5114)
Added
- Add --shuffle option to PonyTest (PR #5076)
Changed
0.62.1
Fix IOCP use-after-free crash
The fix for this issue in 0.62.0 was incomplete. That fix checked for specific Windows error codes (ERROR_OPERATION_ABORTED and ERROR_NETNAME_DELETED) in the IOCP completion callback to detect orphaned I/O operations. However, Windows can deliver completions with other error codes after the socket is closed, and ERROR_NETNAME_DELETED can also arrive from legitimate remote peer disconnects — making error-code matching the wrong approach entirely.
The new fix addresses the root cause: IOCP completion callbacks can fire on Windows thread pool threads after the owning actor has destroyed the ASIO event via pony_asio_event_destroy, leaving the callback with a dangling pointer to freed memory.
Each ASIO event now allocates a small shared liveness token (iocp_token_t) containing an atomic dead flag and a reference count. Every in-flight IOCP operation holds a pointer to the token and increments the reference count. When pony_asio_event_destroy runs, it sets the dead flag (release store) before freeing the event. Completion callbacks check the dead flag (acquire load) before touching the event — if dead, they clean up the IOCP operation struct without accessing the freed event. The last callback to decrement the reference count to zero frees the token.
This correctly handles all error codes and all IOCP operation types (connect, accept, send, recv) without swallowing events the actor needs to see.
Fix pony-lint ignore matching on Windows
pony-lint's .gitignore and .ignore pattern matching failed on Windows because path separator handling was hardcoded to /. On Windows, where paths use \, ignore rules were silently ineffective — files that should have been skipped were linted, and anchored patterns like src/build/ never matched. Windows CI for tool tests has been added to prevent regressions.
Fix pony-lsp on Windows
pony-lsp's JSON-RPC initialization failed on Windows because filesystem paths containing backslashes were embedded directly into JSON strings, producing invalid escape sequences. The LSP file URI conversion also didn't handle Windows drive-letter paths correctly. Additionally, several directory-walking loops in the workspace manager and router used Path.dir to walk up to the filesystem root, terminating when the result was "." — which works on Unix but not on Windows, where Path.dir("C:") returns "C:" rather than ".", causing an infinite loop. Windows CI for tool tests has been added to prevent regressions.
Enforce documented maximum for --ponysuspendthreshold
The help text for --ponysuspendthreshold has always said the maximum value is 1000 ms, but the runtime never actually enforced it. You could pass any value and it would be accepted. Values above ~4294 would silently overflow during an internal conversion to CPU cycles, producing nonsensical thresholds.
The documented maximum of 1000 ms is now enforced. Passing a value above 1000 on the command line will produce an error. Values set via RuntimeOptions are clamped to 1000.
Fix compiler crash when calling methods on invalid shift expressions
The compiler would crash with a segmentation fault when a method call was chained onto a bit-shift expression with an oversized shift amount. For example, y.shr(33).string() where y is a U32 would crash instead of reporting the "shift amount greater than type width" error. The shift amount error was detected internally but the crash occurred before it could be reported. Standalone shift expressions like y.shr(33) were not affected and correctly produced an error message.
Enforce documented bounds for --ponycdinterval
The help text for --ponycdinterval has always said the minimum is 10 ms and the maximum is 1000 ms, but the runtime never actually enforced either bound on the command line. You could pass any non-negative value and it would be silently clamped deep in the cycle detector initialization. Values above ~2147 would also overflow during an internal conversion to CPU cycles, producing nonsensical detection intervals.
The documented bounds are now enforced. Passing a value outside [10, 1000] on the command line will produce an error. Values set via RuntimeOptions continue to be clamped to the valid range.
Add missing NULL checks for gen_expr results in gencall.c
Two additional code paths in the compiler could crash instead of reporting errors when a receiver sub-expression encountered a codegen error. These are the same class of bug as the recently fixed crash when calling methods on invalid shift expressions, but in the gen_funptr and gen_pattern_eq code paths. These are harder to trigger in practice but could cause segfaults in the LLVM optimizer if encountered.
Fix type system soundness hole
The compiler incorrectly accepted aliased type parameters (X!) as subtypes of their unaliased form when used inside arrow types. This allowed code that could duplicate iso references, breaking reference capability guarantees. For example, a function could take an aliased (tag) reference and return it as its original capability (potentially iso), giving you two references to something that should be unique.
Code that relied on this — likely by accident — will now get a type error. The most common pattern affected is reading a field into a local variable and returning it from a method with a this->A return type:
// Before: compiled but was unsound
class Container[A]
var inner: A
fun get(): this->A =>
let tmp = inner
consume tmp
// After: return the field directly instead of going through a local
class Container[A]
var inner: A
fun get(): this->A => innerThe intermediate let binding auto-aliases the type to this->A!, and consuming doesn't undo the alias. Returning the field directly avoids the aliasing entirely.
The persistent list in the collections/persistent package had four signatures using val->A! that relied on this bug. These have been changed to val->A. If you had code implementing the same interfaces with explicit val->A! types, change them to val->A.
Fix cap_isect_constraint returning incorrect capability for empty intersections
When a type parameter was constrained by an intersection of types with incompatible capabilities (e.g., ref and val), the compiler incorrectly computed the effective capability as #any (the universal set) instead of recognizing that no capability satisfies both constraints. This could cause the compiler to silently accept type parameter constraints that have no valid capability, rather than reporting an error.
The compiler now correctly detects empty capability intersections and reports "type parameter constraint has no valid capability" when the intersection of capabilities in a type parameter's constraint is empty. This also fixes incorrect results for iso intersected with #share and #share intersected with concrete capabilities outside its set, which were caused by missing break statements and an incorrect case in the capability intersection logic.
Fix incorrect pool free when tidying the reachability painter
The compiler’s reachability “painter” frees internal colour_record_t nodes when cleaning up. Those allocations must be returned to the same memory pool they came from. A bug passed a size expression as the first argument to POOL_FREE instead of the type name, so the wrong pool index was used when freeing those records.
That could corrupt the allocator’s bookkeeping during compilation in scenarios that exercise that cleanup path. The free now uses the correct type, matching how other POOL_FREE calls work in the codebase.
[0.62.1] - 2026-03-28
Fixed
- Fix IOCP use-after-free crash (PR #5055)
- Fix pony-lint FileNaming false positives on Windows (PR #5059)
- Enforce documented maximum for --ponysuspendthreshold (PR #5061)
- Fix compiler crash when calling methods on invalid shift expressions (PR #5063)
- Enforce documented bounds for --ponycdinterval (PR #5065)
- Fix code generation failure for iftype with union return type (PR #5066)
- Add missing NULL checks for gen_expr results in gencall.c (PR #5067)
- Fix type system soundness hole (PR #4963)
- Fix cap_isect_constraint returning incorrect capability for empty intersections (PR #4999)
- Fix POOL_FREE first argument in painter_tidy (PR #5082)