.release-notes/next-release.md
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.
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.
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.
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.
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 ponylang/ponyc#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.
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.
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.
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.
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.
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
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.
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.
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.
iftype and asPreviously, 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 (ponylang/ponyc#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.
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()?
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.
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.
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 any Pony type, sending a value of one of these aliases across actors needs a sendable capability — the JSON example used for messaging would typically be JsonValue val, with the inner Array and Map constructed inside recover val ... end blocks.
Recursion is allowed when the cycle threads through some generic class's, actor's, or other nominal type's type-argument position (like Array[T] or Map[K, V]) and there's a union somewhere reachable from the cycle with at least one member that doesn't loop back. Without both, the recursion is infinite. The most common illegal shapes:
// Illegal: bare self-reference. The recursive arm has no
// constructor wrapping it, so every value is just None.
type A is (A | None)
// Illegal: recursion through a tuple element. Pony tuples are
// inline value types, so each IntList value would need to inline
// another IntList transitively. No finite layout exists.
type IntList is (None | (U32, IntList))
// Illegal: the recursion threads through Array's type argument,
// but no union with a non-recursive alternative is reachable, so
// there's no base case to stop at.
type A is Array[A]
When you write something illegal, the compiler reports type alias 'X' can't be infinitely recursive along with a note suggesting the fix. Either thread the recursion through the type argument of a generic class (or other nominal type) like Array[X], or add a non-recursive alternative in a union ((None | <body>)).
Pony's error keyword no longer unwinds the stack to propagate errors. Partial functions now return an error flag alongside their result, which try and ? check directly. For pure Pony code the change is invisible: error, try, and ? behave exactly as they did before.
The change is visible at the boundary with C. The pony_error() runtime function has been removed. C code that signalled a Pony error by calling pony_error() must now report failure through its return value, and the Pony side must check that value and raise error itself.
Because pony_error() was the only way a partial FFI function could signal an error, partial FFI is no longer supported. A ? on an FFI declaration (use @foo(...) ?) or on an FFI call (@foo()?) is now a compile error; remove it.
Bare lambdas (@{...}) that raise error outside a try now abort the program rather than unwinding, so a bare partial lambda can no longer propagate an error across a C stack frame.
If you have C code that calls pony_error():
// Before: signal failure by raising a Pony error from C.
PONY_API size_t my_read(...)
{
if(failed)
pony_error();
return bytes_read;
}
// After: report failure through the return value. The mechanism is up
// to you; here, a sentinel the Pony caller knows to treat as failure.
PONY_API size_t my_read(...)
{
if(failed)
return SIZE_MAX;
return bytes_read;
}
// Before: the FFI declaration is partial and the call site uses `?`.
use @my_read[USize](...) ?
fun read(...): USize ? =>
@my_read(...)?
// After: the declaration is not partial; check the sentinel yourself.
use @my_read[USize](...)
fun read(...): USize ? =>
let r = @my_read(...)
if r == USize.max_value() then error end
r
The serialise package has been removed from the standard library. Code that does use "serialise" will no longer compile.
The package was a security footgun: it was only safe when used with fully trusted data, and deserializing untrusted data could crash the program or hand hostile code access to the machine. The capability tokens gating the API did nothing to make deserialization of untrusted input safe. Rather than rework the package, the maintainers chose to remove it. This was ratified as RFC #83.
If you relied on serialise, you will need to implement serialization suited to your own use case and security requirements.
The five PONY_API socket runtime functions — pony_os_writev, pony_os_send, pony_os_recv, pony_os_sendto, and pony_os_recvfrom — have a new signature. Previously they were partial Pony functions returning USize that called pony_error() on failure, with 0 doubling as a "would-block" signal. They now return a three-state result (PONY_SOCKET_OK = 0, PONY_SOCKET_RETRY = 1, PONY_SOCKET_ERROR = 2) and write the operation's byte count through a new trailing size_t* count_out parameter.
Anyone calling these functions from Pony via FFI must update both their use @... declarations and their call sites.
The recommended call-site pattern uses a Pony-side dual of the result type with match \exhaustive\, so a future state addition on the C side surfaces as a compile error rather than a silent fall-through. The Pony stdlib's dual lives at packages/net/_socket_result.pony and is package-private — downstream FFI consumers should define their own dual following the same shape.
Before:
use @pony_os_recv[USize](event: AsioEventID, buffer: Pointer[U8] tag,
size: USize) ?
try
let len = @pony_os_recv(event, buffer, size)?
if len == 0 then
// would-block path
else
// len bytes were received
end
else
// pony_error() fired in the C runtime
end
After:
use @pony_os_recv[U8](event: AsioEventID, buffer: Pointer[U8] tag,
size: USize, count_out: Pointer[USize])
// Define the dual once, in your package:
primitive MyOk fun apply(): U8 => 0
primitive MyRetry fun apply(): U8 => 1
primitive MyError fun apply(): U8 => 2
type MyResult is (MyOk | MyRetry | MyError)
primitive MyResultDecoder
fun apply(v: U8): MyResult =>
match v
| MyOk() => MyOk
| MyRetry() => MyRetry
else MyError
end
// At each call site:
var count: USize = 0
match \exhaustive\ MyResultDecoder(
@pony_os_recv(event, buffer, size, addressof count))
| MyOk => // count holds the bytes received
| MyRetry => // would-block, try again later
| MyError => // unrecoverable error
end
Previously, when a partial method with type parameters was called and the required ? was missing, the compiler emitted the same error twice:
class C
fun f1[A: Any val](x: A): A ? => error
fun f2() ? => f1[U32](42) // missing `?`
main.pony:3:15: call is not partial but the method is - a question mark is required after this call
fun f2() ? => f1[U32](42)
^
Info:
main.pony:2:34: method is here
main.pony:3:15: call is not partial but the method is - a question mark is required after this call
fun f2() ? => f1[U32](42)
^
Info:
main.pony:2:34: method is here
The same duplication occurred for the reverse mistake — a ? on a call to a non-partial method with type parameters. It also occurred for chained .> calls, for partial constructor calls, and when type arguments were filled in from defaults rather than written explicitly. The compiler now emits each diagnostic exactly once.
Additionally, taking the address of a partial method with type arguments (e.g. addressof obj.method[U32]) previously triggered a compiler assertion failure. The same construct now compiles correctly.
#any capability for type parameters with union constraintsA type parameter constrained by a union type — for example,
[O: (Foo tag | Bar ref)] — was being given the #any capability even when
every member's capability was in #alias (one of ref, val, box, or
tag). That made common patterns fail to type-check:
primitive Writer[O: (Async tag | Sync ref)]
fun _write(out: O, data: String) =>
None
fun ok(out: O) =>
_write(out, "+OK\r\n") // error: O #any ! is not a subtype of O #any
The compiler now derives the correct capability for these constraints. In the
example above, the capability is #alias (the smallest capability set that
contains both tag and ref), and the program compiles.
The workaround of intersecting the union with Any #alias is no longer
needed.
The compiler used to silently accept code where two string literals were placed back-to-back with no separator between them, treating them as two unrelated expressions. This most commonly came up as a confusing failure mode for typos involving """, where a missing or misplaced quote produced a program that compiled but behaved nothing like what was written. Adjacent string literals are now reported as a syntax error.
The compiler now rejects is and isnt comparisons whose operand types are two distinct concrete entity types (any pair drawn from class, actor, primitive, and struct). Such comparisons can never be true, so they are almost always a bug; flagging them at compile time surfaces the mistake immediately instead of producing a constant false at runtime.
class C
class D
actor Main
new create(env: Env) =>
let c: C = C
let d: D = D
if c is d then None end // now a compile-time error
The rule fires only when both operands resolve to a single concrete nominal type. Comparisons involving traits, interfaces, unions, intersections, tuples, or type parameters continue to compile, even when one side is concrete. Two reifications of the same generic class (for example Array[U64] and Array[U32]) share a single root definition and are also not flagged. Two object end literals at different source locations synthesize distinct anonymous classes and are flagged.
If you hit the new error, the comparison was unreachable. Switch to structural equality (==) or remove the comparison.
We've updated the LLVM version used to build Pony from 21.1.8 to 22.1.6.
FreeBSD linking now uses the embedded LLD linker (ELF driver) instead of invoking an external compiler driver via system(). You no longer need a C compiler installed solely to link Pony programs on FreeBSD. This is part of the ongoing work to eliminate external linker dependencies across all platforms.
The embedded LLD path activates automatically for any FreeBSD 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.
DragonFly linking now uses the embedded LLD linker (ELF driver) instead of invoking an external C compiler driver via system(). Pkg gcc (e.g. gcc13) is still required at link time to supply libgcc, libgcc_s, libatomic, and the GCC CRT objects — the same install BUILD.md already prescribes for building ponyc on DragonFly. This is part of the ongoing work to eliminate external linker dependencies across all platforms.
OpenBSD linking now uses the embedded LLD linker (ELF driver) instead of invoking an external compiler driver via system(). You no longer need to invoke a C compiler driver solely to link Pony programs on OpenBSD; the base toolchain's clang is still required to build ponyc itself, but ponyc no longer shells out to it at link time. This is part of the ongoing work to eliminate external linker dependencies across all platforms.
The embedded LLD path activates automatically for any OpenBSD 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.