Skip to content

Releases: ponylang/ponyc

0.64.0

30 May 13:13

Choose a tag to compare

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 a CallHierarchyItem describing 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)] => this

A 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 val

After:

let _callback: {(String val): None val} val

Trim 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 = value

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

The 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 complain

The 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...

Read more

0.63.4

02 May 11:32

Choose a tag to compare

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 =>
   None

Hints 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
    end
Error:
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 a TypeHierarchyItem describing it.
  • typeHierarchy/supertypes — returns items for each type in the entity's is (provides) list.
  • typeHierarchy/subtypes — cross-package walk that returns items for every entity whose is list 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/signatureHelp support (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

25 Apr 11:10

Choose a tag to compare

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

  • Add LSP textDocument/selectionRange support (PR #5236)
  • Add LSP workspace/symbol support (PR #5239)

Changed

  • Strengthen memory ordering on runtime queue pushes (PR #5245)

0.63.2

19 Apr 14:05

Choose a tag to compare

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 fun showing the implicit capability (e.g. box) when no explicit cap keyword is written. Not emitted for be or new.
  • 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/refresh support (PR #5224)
  • Extend LSP inlay hints with generic caps, return type caps, and receiver caps (PR #5222)
  • Add LSP textDocument/declaration support (PR #5229)
  • Add LSP textDocument/rename and textDocument/prepareRename support (PR #5228)
  • Add LSP textDocument/typeDefinition support (PR #5231)

0.63.1

11 Apr 15:07

Choose a tag to compare

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 handling

Fix 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())
end

After:

match consume u
| let ut: T =>
  do_something(consume ut)
| let uu: U =>
  (consume uu, default())
end

The else branch becomes | let uu: U => because u is consumed and no longer...

Read more

0.63.0

04 Apr 17:53

Choose a tag to compare

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
    end

This 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
    end
can'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()) // => NaN

After:

// 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 as cast 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.frexp returning 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

  • Remove support for Alpine 3.20 (PR #5094)
  • Remove docgen pass (PR #5097)
  • Change FloatingPoint.frexp exponent return type from U32 to I32 (PR #5113)
  • Use LLVM intrinsics for NaN-propagating float min and max (PR #5114)

0.62.1

28 Mar 19:11

Choose a tag to compare

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 => inner

The 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)

0.62.0

21 Mar 21:08

Choose a tag to compare

Fix incorrect CLOCK_MONOTONIC value on OpenBSD

On OpenBSD, CLOCK_MONOTONIC is defined as 3, not 4 like on FreeBSD and DragonFly BSD. Pony's time package was using 4 for all BSDs, which meant that on OpenBSD, every call to Time.nanos(), Time.millis(), and Time.micros() was actually reading CLOCK_THREAD_CPUTIME_ID — the CPU time consumed by the calling thread — instead of monotonic wall-clock time.

The practical result is that the Timers system, which relies on Time.nanos() for scheduling, would misfire on OpenBSD. Timers set for wall-clock durations would instead fire based on how much CPU time the scheduler thread had burned. For a mostly-idle program, a 60-second timer could take far longer than 60 seconds to fire. Any other code that depends on monotonic time would see similarly wrong values. Hilarity ensues.

Don't allow --path to override standard library

Previously, directories passed via --path on the ponyc command line were searched before the standard library when resolving package names. This meant a --path directory could contain a subdirectory that shadows a stdlib package. For example, --path /usr/lib would cause /usr/lib/debug/ to shadow the stdlib debug package, breaking any code that depends on it.

The standard library is now always searched first, before any --path entries. This is the same fix that was applied to PONYPATH in #3779.

If you were relying on --path to override a standard library package, that will no longer work.

Fix illegal instruction crashes in pony-lint, pony-lsp, and pony-doc

The tool build commands didn't pass --cpu to ponyc, so ponyc targeted the build machine's specific CPU via LLVMGetHostCPUName(). Release builds happen on CI machines that often have newer CPUs than user machines. When a user ran pony-lint, pony-lsp, or pony-doc on an older CPU, the binary could contain instructions their CPU doesn't support, resulting in a SIGILL crash.

The C/C++ side of the build already respected PONY_ARCH (e.g., arch=x86-64 targets the baseline x86-64 instruction set). The Pony code generation for the tools just wasn't wired up to use it. Now it is.

Fix failure to build pony tools with Homebrew

When building ponyc from source with Homebrew, the self-hosted tools (pony-lint, pony-lsp, pony-doc) failed to link because the embedded LLD linker couldn't find zlib. Homebrew installs zlib outside the default system linker search paths, and while CMake resolved the correct location for linking ponyc itself, that path wasn't forwarded to the ponyc invocations that compile the tools.

Fix memory leak

When a behavior was called through a trait reference and the concrete actor's parameter had a different trace-significant capability (e.g., the trait declared iso but the actor declared val), the ORCA garbage collector's reference counting was broken. The sender traced the parameter with one trace kind and the receiver traced with another, causing field reference counts to never reach zero. Objects reachable from the parameter were leaked.

trait tag Receiver
  be receive(b: SomeClass iso)

actor MyActor is Receiver
  be receive(b: SomeClass val) =>
    // b's fields were leaked when called through a Receiver reference
    None

The leak is not currently active because make_might_reference_actor — an optimization that was disabled as a safety net — masks it by forcing full tracing of all immutable objects. Without this fix, the leak would have become active as we started re-enabling that optimization.

This applies to simple nominal parameters as well as compound types — tuples, unions, and intersections — where the elements have different capabilities between the trait and concrete method.

The sender and receiver now use consistent tracing for each parameter, regardless of capability differences between the trait and concrete method.

Fix intermittent hang during runtime shutdown

There was a race condition in the scheduler shutdown sequence that could cause the runtime to hang indefinitely instead of exiting. When the runtime initiated shutdown, it woke all suspended scheduler threads and sent them a terminate message. But a thread could re-suspend before processing the terminate, and once other threads exited, the suspend loop's condition (active_scheduler_count <= thread_index) became permanently true — trapping the thread in an endless sleep cycle with nobody left to wake it.

This was more likely to trigger on slower systems (VMs, heavily loaded machines) where the timing window was wider, and only affected programs using multiple scheduler threads. The fix adds a shutdown flag that the suspend loop checks, preventing threads from re-entering sleep once shutdown has begun.

Fix tuple literals not matching correctly in union-of-tuples types

When a tuple literal was assigned to a union-of-tuples type, inner tuple elements that should have been boxed as union values were stored inline instead. This caused match to produce incorrect results — the match would fail to recognize valid data, or extract wrong values.

type DependencyOp is ((Link, ID) | (Unlink, ID) | Spawn)

type Change is (
  (TimeStamp, Principal, NOP, None)
  | (TimeStamp, Principal, Dependency, DependencyOp)
)

// This produced incorrect match results:
let cc: Change = ((0, 0), "bob", Dependency, (Link, "123"))

// Workaround was to construct the inner tuple separately:
let op: DependencyOp = (Link, "123")
let cc: Change = ((0, 0), "bob", Dependency, op)

Both forms now produce correct results. The fix also applies when tuple literals are passed directly as function arguments.

Fix false positive in _final send checking for generic classes

The compiler incorrectly rejected _final methods that called methods on generic classes instantiated with concrete type arguments. For example, calling a method on a Generic[Prim] where Generic has a trait-constrained type parameter would produce a spurious "_final cannot create actors or send messages" error, even though the concrete type is known and does not send messages.

The finaliser pass now resolves generic type parameters to their concrete types before analyzing method bodies for potential message sends. This expands the range of generic code accepted in _final methods, though some cases involving methods inherited through a provides chain (like Range) still produce false positives.

Fix #share capability constraint intersection

When intersecting a #share generic constraint with capabilities outside the #share set (like ref or box), the compiler incorrectly reported a non-empty intersection instead of recognizing that the capabilities are disjoint. This did not affect type safety — full subtyping checks always ran afterward — but the internal function returned incorrect intermediate results.

Fix use-after-free crash in IOCP runtime on Windows

When a Pony actor closed a TCP connection on Windows, pending I/O completions could fire after the connection's ASIO event and owning actor were already freed, causing an intermittent access violation crash. The runtime now detects these orphaned completions and cleans them up safely without touching freed memory.

Fix pony_os_ip_string returning NULL for valid IP addresses

The runtime function pony_os_ip_string had an inverted check on the result of inet_ntop. Since inet_ntop returns non-NULL on success, the inverted condition meant the function returned NULL for every valid IP address and only attempted to use the result buffer on failure.

Any code that called pony_os_ip_string with a valid IPv4 or IPv6 address got NULL back. The most visible downstream effect was in the ssl library, where X509.all_names() calls this function to convert IP SANs from certificates into strings. The NULL result produced empty strings, corrupting the names array and breaking hostname verification for certificates that use IP SANs.

[0.62.0] - 2026-03-21

Fixed

  • Fix incorrect CLOCK_MONOTONIC value on OpenBSD (PR #5035)
  • Fix illegal instruction crashes in pony-lint, pony-lsp, and pony-doc (PR #5041)
  • Fix failure to build pony tools with Homebrew (PR #5042)
  • Fix memory leak and re-enable immutable-send GC optimization (PR #4944)
  • Fix intermittent hang during runtime shutdown (PR #5037)
  • Fix tuple literals not matching correctly in union-of-tuples types (PR #4970)
  • Fix false positive in _final send checking for generic classes (PR #4982)
  • Fix #share capability constraint intersection (PR #4998)
  • Fix use-after-free crash in IOCP runtime on Windows (PR #5046)
  • Fix pony_os_ip_string returning NULL for valid IP addresses (PR #5049)

Changed

  • Don't allow --path to override standard library (PR #5040)

0.61.1

14 Mar 21:24

Choose a tag to compare

Fix pool_memalign crash due to insufficient alignment for AVX instructions

pool_memalign used malloc() for allocations smaller than 1024 bytes. On x86-64 Linux, malloc() only guarantees 16-byte alignment, but the Pony runtime uses SIMD instructions that require stronger alignment (32-byte for AVX, 64-byte for AVX-512). This caused a SIGSEGV on startup for any program built with -DUSE_POOL_MEMALIGN. Small allocations now use posix_memalign() with 64-byte alignment; large allocations continue to use the full 1024-byte POOL_ALIGN.

Fix assignment-indent to require RHS indented relative to assignment

The style/assignment-indent lint rule previously only checked that multiline assignment RHS started on the line after the =. It did not verify that the RHS was actually indented beyond the assignment line. Code like this was incorrectly accepted:

    let chunk =
    recover val
      Array[U8]
    end

The rule now flags RHS that starts on the next line but is not indented relative to the assignment. The correct form is:

    let chunk =
      recover val
        Array[U8]
      end

Fix tool install with use flags

When building ponyc with use flags (e.g., use=pool_memalign), make install failed because the tools (pony-lsp, pony-lint, pony-doc) were placed in a different output directory than ponyc. Tools now use the same suffixed output directory, and the install target handles missing tools gracefully.

Fix stack overflow in AST tree checker

The compiler's --checktree AST validation pass used deep mutual recursion that could overflow the default 1MB Windows stack when validating programs with deeply nested expression trees. The tree checker has been converted to use an iterative worklist instead.

This is unlikely to affect end users. The --checktree flag is a compiler development tool used primarily when building ponyc itself and running the test suite.

Return memory to the OS when freeing large pool allocations

The pool allocator now returns physical memory to the OS when freeing allocations larger than 1 MB. Previously, freed blocks were kept on the free list with their physical pages committed, meaning the RSS never decreased even after the memory was no longer needed. Now, the page-aligned interior of freed blocks is decommitted via madvise(MADV_DONTNEED) on POSIX or VirtualAlloc(MEM_RESET) on Windows. The virtual address space is preserved, so pages fault back in transparently on reuse.

This primarily benefits programs that make large temporary allocations (the compiler during compilation, programs with large arrays or strings). Programs whose memory usage is dominated by small allocations (< 1 MB) will see little change — per-size-class caching is a separate mechanism.

To preserve the old behavior (never decommit), build with make configure use=pool_retain.

Fix scheduler stats output for memory usage

Applications compiled against the runtime which prints periodic scheduler statistics will now display the correct memory usage values per scheduler and for messages "in-flight".

Fix compiler crash when object literal implements enclosing trait

The compiler crashed with an assertion failure when an object literal inside a trait method implemented the same trait. This code would crash the compiler regardless of whether the trait was used:

trait Printer
  fun double(): Printer =>
    object ref is Printer
      fun apply() => None
    end
  fun apply() => None

This is now accepted. The object literal correctly creates an anonymous class that implements the enclosing trait, and inherited methods that reference the object literal work as expected.

Fix TCPListener accept loop spin on persistent errors

The pony_os_accept FFI function returns a signed int, but TCPListener declared the return type as U32. When accept returned -1 to signal a persistent error (e.g., EMFILE — out of file descriptors), the accept loop treated it as "try again" and spun indefinitely, starving other actors of CPU time.

The FFI declaration now correctly uses I32, and the accept loop bails out on -1 instead of retrying. The ASIO event will re-notify the listener when the socket becomes readable, so no connections are lost.

Update to LLVM 21.1.8

We've updated the LLVM version used to build Pony from 18.1.8 to 21.1.8.

Compile-time string literal concatenation

The compiler now folds adjacent string literal concatenation at compile time, avoiding runtime allocation and copying. This works across chains that mix literals and variables — adjacent literals are merged while non-literal operands are left as runtime .add() calls. For example, "a" + "b" + x + "c" + "d" is folded to the equivalent of "ab".add(x).add("cd"), reducing four runtime concatenations down to two.

Exempt unsplittable string literals from line length rule

The style/line-length lint rule no longer flags lines where the only reason for exceeding 80 columns is a string literal that contains no spaces. Strings without spaces — URLs, file paths, qualified identifiers — cannot be meaningfully split across lines, so flagging them produced noise with no actionable fix.

Strings that contain spaces are still flagged because they can be split at space boundaries using compile-time string concatenation at zero runtime cost:

// Before: flagged, and splitting is awkward
let url = "https://github.com/ponylang/ponyc/blob/main/packages/builtin/string.pony"

// After: no longer flagged — the string has no spaces and can't be split

// Strings with spaces can still be split, so they remain flagged:
let msg = "This is a very long error message that should be split across multiple lines"

// Fix by splitting at spaces:
let msg =
  "This is a very long error message that should be split "
  + "across multiple lines"

Lines inside triple-quoted strings (docstrings) and lines containing """ delimiters are not eligible for this exemption — docstring content should be wrapped regardless of whether it contains spaces.

Fix compiler crash on return error

Previously, writing return error in a function body would crash the compiler with an assertion failure instead of producing a diagnostic error. The compiler now correctly reports that a return value cannot be a control statement.

Add --sysroot option

A new --sysroot option specifies the target system root. The compiler uses the sysroot to locate libc CRT objects and system libraries for the target platform. For native builds, the host root filesystem is used by default; for cross-compilation, common cross-toolchain locations are searched automatically.

Use embedded LLD for Linux targets

All Linux builds — both native and cross-compilation — now use the embedded LLD linker directly instead of invoking an external compiler driver via system(). For cross-compilation, this eliminates the requirement to have a target-specific GCC cross-compiler installed solely for linking.

The embedded LLD path activates automatically for any Linux target without --linker set. To use an external linker instead, pass --linker=<command> as an escape hatch to the legacy linking path. The --link-ldcmd flag is ignored when using embedded LLD; use --linker instead to get legacy behavior.

Fix compilation not correctly triggered upon startup with pony-lsp

It often happens that, during pony-lsp startup, compilation would be triggered before the necessary settings are being provided (e.g. ponypath), so the initial parsing and type-checking of pony-lsp failed. It needed to be retriggered by opening another file or save a currently open file.

This has been fixed by enqueue compilations until settings are provided and only run them if either settings have been provided or we can be sure that no settings can be provided (e.g. if the lsp-client does not implement the necessary protocol bits).

Native macOS builds now use embedded LLD

macOS linking now uses the embedded LLD linker (Mach-O driver) instead of invoking the system ld command. This is part of the ongoing work to eliminate external linker dependencies across all platforms.

If the embedded linker causes issues, use --linker=ld to fall back to the system linker.

Native Windows builds now use embedded LLD

Windows linking now uses the embedded LLD linker (COFF driver) instead of invoking the MSVC link.exe command. This is part of the ongoing work to eliminate external linker dependencies across all platforms.

If the embedded linker causes issues, use --linker=<path-to-link.exe> to fall back to the system linker.

Add OpenBSD 7.8 as a tier 3 CI target

OpenBSD is now a tier 3 (best-effort) platform for ponyc. A weekly CI job builds and tests the compiler, standard library, and tools (pony-doc, pony-lint, pony-lsp) on OpenBSD 7.8.

The tools previously couldn't find the standard library on OpenBSD because they hardcoded their binary name when looking up the executable directory. This works on Linux, macOS, and FreeBSD, which have platform APIs that ignore argv0, but fails on OpenBSD where argv0 is the only mechanism for resolving the executable path. All three tools now pass the real argv[0] from the runtime.

libponyc-standalone is now built on OpenBSD, so programs can link against the compiler as a library on this platform.

Add safety/exhaustive-match lint rule to pony-lint

pony-lint now flags exhaustive match expressions that don't use the \exhaustive\ annotation. Without \exhaustive\, adding a new variant to a union type compiles silently — the compiler injects else None for missing cases instead of raising an error. The new safety/exhaustive-match rule catches these matches so you can add the annotation and get compile-time protection against incomplete case handling.

The rule is enabled by default. To suppress it for a specific match, use // pony-lint: allow safety/exhaustive-match on...

Read more

0.61.0

28 Feb 20:52

Choose a tag to compare

Fix pony-lsp inability to find the standard library

Previously, pony-lsp was unable to locate the Pony standard library on its own. It relied entirely on the PONYPATH environment variable to find packages like builtin. This meant that while the VS Code extension could work around the issue by configuring the path explicitly, other editors using pony-lsp would fail with errors like "couldn't locate this path in file builtin".

pony-lsp now automatically discovers the standard library by finding its own executable directory and searching for packages relative to it — the same approach that ponyc uses. Since pony-lsp is installed alongside ponyc, the standard library is found without any manual configuration, making pony-lsp work out of the box with any editor.

Fix persistent HashMap returning incorrect results for None values

The persistent HashMap used None as an internal sentinel to signal "key not found" in its lookup methods. This collided with user value types that include None (e.g., Map[String, (String | None)]). Using HashMap with a None value could lead to errors in user code as "it was none" and "it wasn't present" were impossible to distinguish.

The internal lookup methods now use error instead of None to signal a missing key, so all value types work correctly.

This is a breaking change for any code that was depending on the previous (incorrect) behavior. For example, code that expected apply to raise for keys mapped to None, or that relied on contains returning false for None-valued entries, will now see correct results instead.

Add pony-lint to the ponyc distribution

pony-lint is a text-based linter for Pony source files that checks for style guide violations. It was previously a standalone project and is now distributed with ponyc. This means the linter will track changes in ponyc and will always be up-to-date with the compiler's version of the standard library. pony-lint currently checks for line length, trailing whitespace, hard tabs, and comment spacing violations.

Fix stack overflow in reachability pass on deeply nested ASTs

The reachability pass traversed AST trees using unbounded recursion, adding a stack frame for each child node. On Windows x86-64, which has a default stack size of 1MB, this could overflow when compiling programs with large type graphs that produce deeply nested ASTs (200+ frames deep).

The recursive child traversal is now iterative, using an explicit worklist. This removes the dependency on call stack depth for AST traversal in the reachability pass.

Add pony-doc tool

We've added an experimental new tool, pony-doc, that is intended to replace the --docs pass in ponyc. The docs pass will remain in ponyc for now but will eventually be removed.

The most notable difference from the existing --docs flag is that pony-doc generates documentation for public items only by default. To include private types and methods in the output, use the --include-private flag. This replaces the old --docs-public flag which worked in reverse — generating everything by default and requiring a flag to restrict to public items only.

Usage:

pony-doc [options] <package-directory>

Options:

  • -o, --output: Output directory (default: current directory)
  • --include-private: Include private types and methods
  • -V, --version: Print version and exit

Add Support for Settings to pony-lsp

pony-lsp can now be provided with settings from the lsp-client (most of the time this is the editor).
It supports two settings, which are both optional:

  • defines: A list of strings that will be defined during compilation and can be checked for with ifdef. Those defines are usually provided to ponyc with the -D flag.
  • ponypath: A string, containing a path or list of paths (like used e.g. in the $PATH environment variable) that will be used to tell pony-lsp about additional entries to its package search path.

Example settings as JSON:

{
  "defines": ["FOO", "BAR"],
  "ponypath": "/path/to/pony-package:/another/path"
}

Improve pony-lsp diagnostics

pony-lsp only relayed the main message from errors provided by ponyc. Now, it also passes the additional information to help explain the error.

Make pony-lsp version dynamic to match ponyc version

Previously, the version of pony-lsp was hardcoded in the source code. Now it is dynamically generated from the ponyc version.

Add \exhaustive\ annotation for match expressions

A new \exhaustive\ annotation can be applied to match expressions to assert that all cases are explicitly handled. When present, the compiler will emit an error if the match is not exhaustive and no else clause is provided, instead of silently injecting an else None.

Without the annotation, a non-exhaustive match silently compiles with an implicit else None, which changes the result type to (T | None) and produces an indirect type error like "function body isn't the result type." With the annotation, the error message directly identifies the problem.

type Choices is (T1 | T2 | T3)

primitive Foo
  fun apply(p: Choices): String =>
    match \exhaustive\ p
    | T1 => "t1"
    | T2 => "t2"
    end

The above produces: match marked \exhaustive\ is not exhaustive

Adding the missing case or an explicit else clause resolves the error. The annotation is also useful on matches that are already exhaustive as a future-proofing measure. Without it, if a new member is later added to the union type and the match isn't updated, the compiler silently injects else None. You'll only get an error if the match result is assigned to a variable whose type doesn't include None -- otherwise the bug is completely silent. With \exhaustive\, the compiler catches the missing case immediately.

Update Docker image base to Alpine 3.23

The ponylang/ponyc:nightly and ponylang/ponyc:release Docker images now use Alpine 3.23 as their base image, updated from Alpine 3.21.

[0.61.0] - 2026-02-28

Fixed

  • Fix pony-lsp inability to find the standard library (PR #4829)
  • Make pony-lsp version dynamic to match ponyc version (PR #4830)
  • Fix persistent HashMap returning incorrect results for None values (PR #4839)
  • Fix stack overflow in reachability pass on deeply nested ASTs (PR #4858)

Added

  • Add pony-lint to the ponyc distribution (PR #4842)
  • Add configuration from lsp-client and improve diagnostics handling (PR #4837)
  • Add \exhaustive\ annotation for match expressions (PR #4863)

Changed

  • Changed persistent hash map apply signature (PR #4839)
  • Update Docker image base to Alpine 3.23 (PR #4887)