github ponylang/ponyc 0.65.0

3 hours ago

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 end

Using 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)
    end

Previously 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 end

This is fixed:

  • A break that carries a value gives its loop both a value and an exit, so the loop no longer crashes when its else clause jumps away — it compiles and yields the break value (case 3). The same is true of a continue that reaches a value-producing else. This applies to both while and repeat.
  • 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 else clause, or as a break value 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 external linker.

Fix use=dtrace builds on macOS

Building ponyc with use=dtrace failed on macOS because the build tried to run dtrace -G, a flag that macOS dtrace has never supported. macOS's linker resolves DTrace probe symbols natively, so the -G step was never needed — only the dtrace -h header generation step is required.

use=dtrace now builds and links correctly on macOS. Programs compiled by a dtrace-enabled ponyc expose their pony provider probes to DTrace. Actually tracing a running program requires System Integrity Protection (SIP) to permit DTrace; see the examples/dtrace README for details.

Fix intermittent crashes when compiling on multiple threads at once

The compiler could crash intermittently when more than one compilation ran at the same time within a single process, as the Pony language server does while you edit. Because the failure depended on thread timing, it appeared as occasional, hard-to-reproduce crashes rather than a consistent error.

Concurrent compilations no longer share state that simultaneous access could corrupt, so these crashes no longer happen.

Reject self-referential iftype constraints inside lambdas and object literals

A self-referential iftype subtype condition — one whose subtype check refers back to the tested type parameter through a union, intersection, or tuple — is meaningless and is rejected when written in a method. The same condition inside a lambda or object literal was silently accepted. It is now rejected everywhere with the same error.

The following program previously compiled but now reports a clear error:

actor Main
  new create(env: Env) =>
    let f = {[A](x: A) =>
      iftype A <: (A | None) then None end}
    f[U8](0)

Reject use=dtrace at configure time on DragonFly and OpenBSD

DTrace isn't supported on DragonFly BSD or OpenBSD. make configure use=dtrace now rejects the option on those platforms with a clear, platform-specific message rather than a confusing build failure or a generic error.

Remove the --linker and --link-ldcmd command line options

ponyc now links every supported configuration with its embedded LLD linker, so the options that selected an external linker have been removed. --link-ldcmd already had no effect on the embedded linker, and --linker was an escape hatch back to the external linker, which no longer exists.

If you used --linker to link an additional library, declare it in your Pony source instead.

Before:

ponyc --linker="ld -lFoo"

After:

use "lib:Foo"

use "path:..." adds a library search path the same way. There is no longer a way to pass arbitrary linker flags directly; if your build relies on that, let us know in this discussion.

Fix incorrect rejection and crashes for iftype conditions inside lambdas and object literals

An iftype condition behaved differently inside a lambda or object literal than it does at method scope, in two ways that are now fixed.

A condition that narrows a type parameter and then uses that narrowed parameter in the then branch compiled at method scope but was wrongly rejected inside a lambda or object literal. This was most visible with a recursively-constrained trait — for example trait T[X: T[X]] with the condition iftype A <: T[A], which failed with "type argument is outside its constraint" — but it affected any narrowing condition whose narrowed parameter was used in the branch.

A let or var binding in the then branch of such a condition crashed the compiler with an internal assertion failure instead of compiling.

Both now behave inside a lambda or object literal exactly as they do at method scope. The following program previously failed to compile but now works:

trait T[X: T[X] #any]
  fun tag m(): X

class val C is T[C]
  fun tag m(): C => C.create()

actor Main
  new create(env: Env) =>
    let f = {[A](x': A) =>
      var x = x'
      iftype A <: T[A] then
        x = x.m()
      end}
    f[C](C)

Fix runtime crash in optimized builds for types with 128-bit fields

Programs containing a class or struct with a U128 or I128 field could crash with a segmentation fault at runtime when compiled in release (optimized) mode, even though the same program ran correctly when compiled with --debug. The crash happened when such an object was used in a way that let the compiler place it on the stack.

These objects are now correctly aligned in optimized builds, and the crash no longer occurs.

Add Alpine 3.24 as a supported platform

We've added arm64 and amd64 builds for Alpine Linux 3.24. We'll be building ponyc releases for it until it stops receiving security updates in 2028. At that point, we'll stop building releases for it.

Fix UDPSocket.set_multicast_interface not setting the interface

UDPSocket.set_multicast_interface never actually set the interface used for outgoing multicast. It handed the operating system the address of an internal pointer rather than the resolved interface address, so the kernel received meaningless bytes and the socket's multicast interface was left unchanged, on both IPv4 and IPv6.

It now sets the interface correctly. For an IPv4 interface, pass the interface's IPv4 address. For an IPv6 interface, the interface is taken from the scope id of the resolved address, so a scoped address such as "fe80::1%eth0" is needed to select an interface.

Fix UDPSocket.set_multicast_loopback and set_multicast_ttl having no effect

UDPSocket.set_multicast_loopback and set_multicast_ttl did not change a socket's IPv4 multicast loopback or TTL behavior. They applied the options at the wrong socket level, so the request either failed or set an unrelated option, leaving the multicast loopback and TTL at their defaults.

Both now take effect for IPv4 multicast.

Fix Windows process crash when a UDP socket fails to listen

On Windows, a UDPSocket that failed to start listening — for example because its host address could not be resolved — would crash the entire process immediately after reporting the failure through not_listening. The failure is now reported and your program keeps running, matching the behavior on other platforms.

Make UDPSocket.set_broadcast a no-op on IPv6 sockets

UDPSocket.set_broadcast promises to enable or disable broadcasting from a socket. On IPv4 sockets it does: it sets the SO_BROADCAST socket option to the value you pass. On IPv6 sockets it instead ignored its argument and joined the FF02::1 all-nodes multicast group — set_broadcast(false) joined the group too, and the group was never left.

IPv6 has no broadcast, and sending to a multicast address such as the all-nodes group (DNS.broadcast_ip6) requires no permission, so there is nothing for set_broadcast to enable on an IPv6 socket. It is now a documented no-op on IPv6 sockets. The removed join had no observable effect in practice — every IPv6 node is automatically a member of the all-nodes group — so existing programs should see no change in behavior. To receive traffic for a multicast group, use multicast_join.

Fix crash when a directory is named like a Pony source file

Previously, if a directory inside a package was named like a Pony source file — that is, with a name ending in .pony — ponyc would abort with an out-of-memory error instead of compiling your program. Such directories are now ignored, like any other non-source entry in a package directory, and compilation proceeds normally.

Fix NetAddress.scope() returning a byte-swapped value on little-endian platforms

NetAddress.scope() returned the IPv6 scope zone identifier -- the interface index of a scoped address, such as the eth0 in fe80::1%eth0 -- with its bytes reversed on little-endian platforms. An address scoped to interface index 2, for instance, returned 33554432 (0x02000000) instead of 2, making the value useless for identifying the interface.

scope() now returns the zone identifier correctly. Big-endian platforms were unaffected and are unchanged.

Fix crash when tracing actor behaviors without forced actor tracing

A runtime built with runtime_tracing enabled would crash when the actor_behavior tracing category was active without --ponytracingforceactortracing also being set:

program --ponytracingcategories actor_behavior

The crash occurred whenever a message was scheduled from a thread that was not running an actor, such as the ASIO thread delivering a network event or the cycle detector. Programs that perform any I/O or run long enough to trigger cycle detection would reliably crash. Setting --ponytracingforceactortracing all avoided the crash.

This has been fixed. Actor behavior tracing now works without forcing actor tracing on.

Fix unnecessary reallocations when building small Strings and Arrays

Building a small String or Array incrementally — appending or pushing through its first several elements — triggered repeated reallocations and copies, even though every one of those sizes fit within the single block the runtime allocator had already handed out. The collections now record the capacity of the block they were given, so a small String or Array allocates once and does not reallocate again until it genuinely outgrows that block.

A side effect is that space() may report a larger capacity than before for a small collection; the extra capacity is memory the allocator had already reserved, so a program's memory use is unchanged.

Fix linking of Pony programs on 32-bit ARM Linux

Pony programs failed to link on 32-bit ARM Linux systems, such as a Raspberry Pi running a 32-bit OS. They now build and run correctly.

Compile C shims alongside Pony

Sometimes the easiest way to use a C library from Pony is a small piece of C: a wrapper that flattens a macro into a function, fixes up a calling convention, or adapts a struct-heavy API into something FFI-friendly. Until now that meant setting up a second build system to compile the C and a use "lib:..." directive to link it.

Now ponyc compiles the C for you. A .c file placed in a package's directory (next to its .pony files) is a C shim: ponyc discovers it, compiles it with an embedded copy of clang, and links the object into your program. No Makefile, no separate compiler invocation, no use "lib:...".

// main.pony
use "cdefine:SHIM_ANSWER=42"

use @shim_answer[I32]()

actor Main
  new create(env: Env) =>
    env.out.print(@shim_answer().string())
// shim.c, in the same directory
#include <stdint.h>

int32_t shim_answer(void)
{
  return SHIM_ANSWER;
}

ponyc's own headers are on the include path by default, resolved relative to the running compiler the same way the standard library is found — so a shim can #include <pony.h> and call runtime APIs without any configuration, and without hard-coding an installation path that breaks on the next toolchain update.

pony.h is the supported header for shims. ponyc's other internal headers that a shim might reach — ponyassert.h (for pony_assert) and the platform.h closure it pulls in — are also on the include path and shipped with an installed ponyc, but they are internal headers offered as a convenience, not a stable interface: they can change between releases without notice. Build against pony.h; reach for the others only if you accept that they may move.

Two new use schemes configure how a package's shims are compiled. use "cdefine:NAME" or use "cdefine:NAME=VALUE" defines a C preprocessor macro (clang's -D), and use "cincludedir:PATH" adds an include search directory (clang's -I), with relative paths resolved against the package's directory. Both apply only to the package that declares them — putting one in a package with no .c files is an error, since it could never take effect; the C source it was meant for is usually in a different package. Both accept guards: use "cdefine:USE_EPOLL" if linux. Defining the same macro name twice in one package is an error, whether or not the values match — platform-specific values belong behind guards, where only the active target's definition counts. The macro name must be a plain C identifier: function-like macros (cdefine:CALLBACK(x)=...), which clang's -D accepts, are rejected — define a regular macro and let the C do the rest. Note that cdefine: is a C preprocessor macro for shims only; it is unrelated to ponyc's own --define/-D flag, which drives Pony's ifdef.

The shim sources themselves have no per-file guard: every .c in the package directory is compiled on every platform. A platform-specific shim wraps its whole body in #ifdef so it compiles to an empty object elsewhere. On macOS, shims resolve system headers through the SDK's usr/include (framework-style includes, clang's -F, are not supported); on Windows, through the installed MSVC and Windows SDK include directories.

Shim objects are linked directly, not archived into a library first. That has two consequences worth knowing. First, C constructors (__attribute__((constructor))) in shims run at program start, like any directly linked object. Second, two shims defining the same symbol fail the link loudly with a duplicate-definition error — and a shim that also defines a symbol from a use "lib:..." library silently wins when that library is a static archive (the common case: the shim object satisfies the symbol, so the archive member is never pulled in). A lib: that names a whole object file or a shared library behaves differently — a duplicate definition in a whole object is a loud link error, and a shared library still loads at runtime — so don't rely on the shadow for those; use the migration below.

That last point is the migration warning: if you previously compiled a .c next to your Pony code into a library by hand and linked it with use "lib:...", ponyc now also compiles that file as a shim, and the shim object silently shadows your hand-built library. Either delete the manual build and the use "lib:..." directive (the shim path replaces both), or move the .c out of the package directory (a subdirectory works — only the package's top directory is scanned).

C compile errors are reported like any other ponyc error, with the file, line, and column. Shims are recompiled on every build; their objects live in the output directory during the build and are cleaned up after a successful link. Under non-link modes (--pass c, --pass obj, and friends) or after a failed link the objects stay behind, and renaming or deleting a shim source can leave an old object in the output directory — they're plain files, safe to delete. Because ponyc now carries clang inside it, the compiler binary is noticeably larger than before.

A C shim is the package doing C, so --safe governs it exactly like a C FFI call: compiling a shim requires the package to be allowed to do C FFI. A .c in a package that isn't on the --safe list is an error, the same one you'd get for an FFI call there. The file is allowed to sit in the package; what's gated is compiling it.

Fix scheduler threads suspending after half the configured idle time

The --ponysuspendthreshold runtime option controls how long a scheduler thread waits, while it has no work to do, before suspending itself to reduce CPU usage. It is documented in milliseconds and defaults to 1 ms.

On the most common platforms, including all x86 systems, the threshold was being applied at half its intended scale, so threads started suspending after about half the idle time you asked for. Passing --ponysuspendthreshold 100 caused threads to begin suspending after roughly 50 ms instead of 100 ms, and the 1 ms default behaved like 0.5 ms.

The threshold now uses the same timing scale as the runtime's other interval options, such as --ponycdinterval, rather than half of it.

As a result, idle scheduler threads now stay awake roughly twice as long before suspending compared to previous releases. Programs that depend on scheduler threads suspending quickly when idle (for example, to keep CPU usage low on an otherwise idle process) may see threads remain active a little longer, while programs sensitive to the cost of waking a suspended thread when new work arrives may benefit. If you previously tuned --ponysuspendthreshold, halve your value to keep the same timing as before.

The same calibration error affected an internal threshold the runtime uses to decide a scheduler thread has gone idle, which doubled along with the suspend threshold. As a side effect, a program that goes idle may take a fraction of a millisecond longer to be detected as ready to terminate than in previous releases.

Fix scheduler timing calculation bugs

The runtime scheduler makes several decisions based on how much time has elapsed, measured with a CPU cycle counter. Two of those calculations were wrong.

On 32-bit ARM, the counter is a hardware cycle counter that periodically wraps back to zero. Several scheduler checks subtracted two readings without accounting for the wrap, so for one iteration after each wrap the elapsed time came out as a nonsensical huge value — briefly suspending a scheduler thread early, sending an internal "no more work" notification early, or printing runtime statistics ahead of schedule before self-correcting. The runtime now accounts for the wrap, so the scheduler no longer misreads elapsed time as the counter wraps.

Separately, on every platform, the runtime statistics interval (available when the runtime is built with stats tracking and selected with the stats-interval option) was computed at the wrong scale and at too narrow an integer width: statistics printed roughly a thousand times more often than the requested interval, and a large interval could overflow to a small value. Statistics now print at the requested interval.

Programs that run on other platforms and don't enable runtime statistics were unaffected.

Fix brief scheduler busy-spin after a long idle on 32-bit ARM

On 32-bit ARM, a scheduler thread that had been idle (suspended) for longer than a couple of minutes could, on waking, briefly burn CPU before suspending again instead of re-suspending promptly. This happened because the CPU cycle counter the scheduler uses to measure idle time wraps during such a long idle. Idle scheduler threads on 32-bit ARM now re-suspend promptly no matter how long they have been idle. Programs on other platforms were unaffected.

Truncate oversized UDP datagrams on Windows instead of dropping them

On Windows, a UDP datagram larger than the receiving socket's read buffer was delivered to UDPSocket's notifier as an empty array, silently dropping the entire datagram. On Linux, macOS, and the BSDs the same datagram is delivered truncated to the buffer's first bytes, with only the excess discarded.

Windows now matches that behavior: an oversized datagram is delivered truncated to the buffer's first bytes instead of being dropped entirely. This does not prevent data loss -- the bytes beyond the buffer are still discarded, on every platform. To receive a datagram whole, size the UDPSocket read buffer (the size argument to the listen constructors) to your protocol's largest expected datagram.

Fix compiler crash when compiling very long expression sequences

The compiler could crash when compiling a very long sequence of expressions, such as a large array or tuple literal, a long argument list, or a long function or method body — the kind of thing code generators often produce. Sequences of this kind now compile without crashing, regardless of how many expressions they contain.

Update supported FreeBSD 15 to 15.1

We've moved our supported FreeBSD 15 release from 15.0 to 15.1. ponyc is now built and tested against FreeBSD 15.1.

Make systematic testing reproducible with multiple scheduler threads when scheduler scaling is disabled

Systematic testing (use=systematic_testing) lets you replay a runtime interleaving from a seed: run until an intermittent bug shows up, then re-run with --ponysystematictestingseed <seed> to replay the same interleaving and chase it down. Previously this only held with a single scheduler thread (--ponymaxthreads 1), and a single thread explores no interleavings at all, so reproducibility and interleaving exploration were mutually exclusive.

With scheduler scaling disabled (--ponynoscale), a fixed seed now replays the same interleaving across runs with more than one scheduler thread, while different seeds still produce different interleavings. The cycle detector can stay enabled. Making dynamic scheduler scaling itself reproducible under systematic testing is not yet covered; use --ponynoscale for reproducible multi-threaded replay until then.

Make systematic testing replay independent of memory layout

Under use=systematic_testing, replaying a run from a fixed --ponysystematictestingseed is meant to reproduce the same scheduler interleaving. Previously this could fail for programs with enough actor-to-actor messaging to trigger backpressure: the replay depended on the process's memory layout, so address-space layout randomization (ASLR) made the same seed produce different interleavings from one run to the next, even with --ponynoscale and a fixed thread count. A fixed seed now replays the same interleaving regardless of memory layout.

Make systematic testing replay independent of memory layout for reference-counting messages

Under use=systematic_testing, replaying a run from a fixed --ponysystematictestingseed is meant to reproduce the same scheduler interleaving. For workloads that pass references between actors — anything that drives the runtime's reference-counting (acquire/release) messages — a fixed seed could still replay a different interleaving from one run to the next, because those messages were sent in an order that depended on actor memory addresses, which the operating system randomizes per run. Replaying such a workload now reproduces the same interleaving regardless of address-space layout.

This builds on the earlier fix for the actor muting path. With both in place and the cycle detector disabled (--ponynoblock), a fixed seed replays deterministically for these workloads; the cycle detector has its own remaining layout dependence, tracked separately.

Fix an intermittent hang when using systematic testing

Programs run under systematic testing (--ponysystematictestingseed) could
intermittently hang instead of running to completion. The hang was timing
sensitive: the same seed might hang on one run and finish normally on another.
This has been fixed.

Support building with systematic testing on Windows

The runtime's systematic testing mode can now be built and run on Windows. Previously it was only available on Linux and macOS: the Windows build script had no way to turn on runtime use options, and the systematic testing runtime had never been compiled for Windows.

Enable it by passing -Use systematic_testing to make.ps1 configure. Unlike the Linux and macOS builds, Windows does not pair it with scheduler_scaling_pthreads — it is enabled on its own, because Windows scales the scheduler with native primitives rather than pthreads.

.\make.ps1 configure -Config Debug -Use systematic_testing
.\make.ps1 build -Config Debug

A program built this way replays a single scheduler interleaving from a fixed --ponysystematictestingseed, the same as on the other platforms.

Make systematic testing replay independent of memory layout for cycle detector messages

Under use=systematic_testing, replaying a run from a fixed --ponysystematictestingseed is meant to reproduce the same scheduler interleaving. Any workload that exercised the cycle detector — actors blocking and unblocking, and reference cycles forming and being reclaimed — could still replay a different interleaving from one run to the next, because the detector issued its messages (the probes it sends while checking which actors have blocked, the confirmations it sends to a cycle's members, and the reference-count releases it sends when reclaiming a cycle) in an order that depended on actor memory addresses, which the operating system randomizes per run. Replaying such a workload now reproduces the same interleaving regardless of address-space layout.

[0.65.0] - 2026-06-27

Fixed

  • Fix use=dtrace builds on FreeBSD (PR #5400)
  • Fix compiler crashes from control expressions that jump away in value positions (PR #5406)
  • Reject self-referential type parameter constraints (PR #5403)
  • Report an error for infinitely recursive generic types (PR #5411)
  • Fix compiler crash on partial application of a method with a literal default argument (PR #5412)
  • Fix compiler crashes in while and repeat loops that jump away (PR #5418)
  • Reject tuple types hidden in an intersection within a type constraint (PR #5439)
  • Fix use=dtrace builds on macOS (PR #5445)
  • Fix intermittent crashes when compiling on multiple threads at once (PR #5420)
  • Reject self-referential iftype constraints inside lambdas and object literals (PR #5423)
  • Fix incorrect rejection and crashes for iftype conditions inside lambdas and object literals (PR #5443)
  • Fix runtime crash in optimized builds for types with 128-bit fields (PR #5464)
  • Fix UDPSocket.set_multicast_interface not setting the interface (PR #5481)
  • Fix UDPSocket.set_multicast_loopback and set_multicast_ttl having no effect (PR #5481)
  • Fix Windows process crash when a UDP socket fails to listen (PR #5483)
  • Make UDPSocket.set_broadcast a no-op on IPv6 sockets (PR #5497)
  • Fix crash when a directory is named like a Pony source file (PR #5514)
  • Fix NetAddress.scope() returning a byte-swapped value on little-endian platforms (PR #5511)
  • Fix crash when tracing actor behaviors without forced actor tracing (PR #5522)
  • Fix unnecessary reallocations when building small Strings and Arrays (PR #5518)
  • Fix linking of Pony programs on 32-bit ARM Linux (PR #5526)
  • Fix scheduler threads suspending after half the configured idle time (PR #5535)
  • Fix scheduler timing calculation bugs (PR #5538)
  • Fix brief scheduler busy-spin after a long idle on 32-bit ARM (PR #5539)
  • Truncate oversized UDP datagrams on Windows instead of dropping them (PR #5552)
  • Fix compiler crash when compiling very long expression sequences (PR #5553)
  • Make systematic testing reproducible with multiple scheduler threads when scheduler scaling is disabled (PR #5561)
  • Make systematic testing replay independent of memory layout (PR #5566)
  • Make systematic testing replay independent of memory layout for reference-counting messages (PR #5570)
  • Fix an intermittent hang when using systematic testing (PR #5576)
  • Make systematic testing replay independent of memory layout for cycle detector messages (PR #5580)

Added

  • Add JsonPrinter for serializing any JsonValue (PR #5397)
  • Add Alpine 3.24 as a supported platform (PR #5466)
  • Compile C shims alongside Pony (PR #5503)
  • Support building with systematic testing on Windows (PR #5574)

Changed

  • Use embedded LLD for native Linux sanitizer builds (PR #5409)
  • Remove serialization methods from JsonObject and JsonArray (PR #5397)
  • Rename JsonObject and JsonArray serialization methods (PR #5397)
  • Use embedded LLD for native FreeBSD sanitizer builds (PR #5426)
  • Use embedded LLD for FreeBSD use=dtrace builds (PR #5440)
  • Update supported OpenBSD version to 7.9 (PR #5442)
  • Use embedded LLD for native macOS sanitizer builds (PR #5444)
  • Reject use=dtrace at configure time on DragonFly and OpenBSD (PR #5449)
  • Remove the --linker and --link-ldcmd command line options (PR #5452)
  • Update supported FreeBSD 15 to 15.1 (PR #5555)

Don't miss a new ponyc release

NewReleases is sending notifications on new releases.