Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,3 @@ packages/stdlib/stdlib
tools/pony-lsp/version.pony
tools/pony-lint/version.pony
tools/pony-doc/version.pony

# Allow these paths
!src/libponyrt/lang/except_try_catch.ll
114 changes: 114 additions & 0 deletions .release-notes/next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,117 @@ 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>)`).

## Replace stack-unwinding error handling with error-flag returns

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()`:

```c
// Before: signal failure by raising a Pony error from C.
PONY_API size_t my_read(...)
{
if(failed)
pony_error();
return bytes_read;
}
```

```c
// 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;
}
```

```pony
// Before: the FFI declaration is partial and the call site uses `?`.
use @my_read[USize](...) ?

fun read(...): USize ? =>
@my_read(...)?
```

```pony
// 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
```

## Remove the serialise package from the standard library

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.

## Change socket runtime functions to use three-state result type

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:
```pony
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:
```pony
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
```

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ All notable changes to the Pony compiler and standard library will be documented

### Changed

- Replace stack-unwinding error handling with error-flag returns ([PR #5002](https://github.com/ponylang/ponyc/pull/5002))
- Remove the serialise package from the standard library ([PR #5002](https://github.com/ponylang/ponyc/pull/5002))
- Change socket runtime functions to use three-state result type ([PR #5002](https://github.com/ponylang/ponyc/pull/5002))

## [0.63.4] - 2026-05-02

Expand Down
29 changes: 29 additions & 0 deletions packages/net/_socket_result.pony
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Pony-side dual of `pony_socket_result_t` defined in
// `src/libponyrt/lang/socket.h`. The integer values returned from `apply()`
// must match the C-side `PONY_SOCKET_OK`/`PONY_SOCKET_RETRY`/`PONY_SOCKET_ERROR`
// constants, which are part of the FFI ABI for the five `pony_os_*` socket
// runtime functions. Keep both files in sync.
//
// `_SocketResultDecoder` collapses any out-of-range U8 to `_SocketResultError`
// so unknown C-side values fail closed. Adding a new wire value on the C side
// requires updating both the `_SocketResult` union and this decoder.

primitive _SocketResultOk
fun apply(): U8 => 0

primitive _SocketResultRetry
fun apply(): U8 => 1

primitive _SocketResultError
fun apply(): U8 => 2

type _SocketResult is
(_SocketResultOk | _SocketResultRetry | _SocketResultError)

primitive _SocketResultDecoder
fun apply(v: U8): _SocketResult =>
match v
| _SocketResultOk() => _SocketResultOk
| _SocketResultRetry() => _SocketResultRetry
else _SocketResultError
end
36 changes: 36 additions & 0 deletions packages/net/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ actor \nodoc\ Main is TestList
fun tag tests(test: PonyTest) =>
// Tests below function across all systems and are listed alphabetically
test(_TestOsIpString)
test(_TestSocketResultDecoder)
test(_TestTCPConnectionFailed)
test(_TestTCPExpect)
test(_TestTCPExpectOverBufferSize)
Expand Down Expand Up @@ -127,6 +128,41 @@ class \nodoc\ _TestPong is UDPNotify
recover val [[U8('p'); U8('o'); U8('n'); U8('g'); U8('!')]] end,
from)

class \nodoc\ iso _TestSocketResultDecoder is UnitTest
"""
Verify _SocketResultDecoder maps every U8 to the expected union variant.
The wire values 0/1/2 must match the C-side PONY_SOCKET_OK/RETRY/ERROR
in `src/libponyrt/lang/socket.h`. Anything else falls through to Error
so unknown C-side values fail closed.
"""
fun name(): String => "net/SocketResultDecoder"

fun ref apply(h: TestHelper) =>
// Anchor the wire contract: the apply() values must equal the
// PONY_SOCKET_* constants in socket.h.
h.assert_eq[U8](_SocketResultOk(), 0)
h.assert_eq[U8](_SocketResultRetry(), 1)
h.assert_eq[U8](_SocketResultError(), 2)

// Sweep every U8: 0 → Ok, 1 → Retry, anything else → Error.
var v: U8 = 0
while true do
let r = _SocketResultDecoder(v)
match v
| 0 =>
h.assert_true(r is _SocketResultOk,
"decoder(" + v.string() + ") should be Ok")
| 1 =>
h.assert_true(r is _SocketResultRetry,
"decoder(" + v.string() + ") should be Retry")
else
h.assert_true(r is _SocketResultError,
"decoder(" + v.string() + ") should be Error")
end
if v == U8.max_value() then break end
v = v + 1
end

class \nodoc\ iso _TestBroadcast is UnitTest
"""
Test broadcasting with UDP.
Expand Down
109 changes: 65 additions & 44 deletions packages/net/tcp_connection.pony
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use @pony_asio_event_resubscribe_write[None](event: AsioEventID)
use @pony_asio_event_get_disposable[Bool](event: AsioEventID)
use @pony_asio_event_set_writeable[None](event: AsioEventID, writeable: Bool)
use @pony_asio_event_set_readable[None](event: AsioEventID, readable: Bool)
use @pony_os_recv[USize](event: AsioEventID, buffer: Pointer[U8] tag,
size: USize) ?
use @pony_os_writev[USize](ev: AsioEventID, wsa: Pointer[(USize, Pointer[U8] tag)] tag,
wsacnt: I32) ? if windows
use @pony_os_writev[USize](ev: AsioEventID, iov: Pointer[(Pointer[U8] tag, USize)] tag,
iovcnt: I32) ? if not windows
use @pony_os_recv[U8](event: AsioEventID, buffer: Pointer[U8] tag,
size: USize, count_out: Pointer[USize])
use @pony_os_writev[U8](ev: AsioEventID, wsa: Pointer[(USize, Pointer[U8] tag)] tag,
wsacnt: I32, count_out: Pointer[USize]) if windows
use @pony_os_writev[U8](ev: AsioEventID, iov: Pointer[(Pointer[U8] tag, USize)] tag,
iovcnt: I32, count_out: Pointer[USize]) if not windows
use @pony_os_writev_max[I32]()
use @pony_os_keepalive[None](fd: U32, secs: U32)
use @pony_os_socket_close[None](fd: U32)
Expand Down Expand Up @@ -473,16 +473,15 @@ actor TCPConnection is AsioEventNotify
end

// Write as much data as possible.
// Returns how many we sent or 0 if we are experiencing backpressure
let len =
var count: USize = 0
match \exhaustive\ _SocketResultDecoder(
@pony_os_writev(_event,
_pending_writev_windows.cpointer(_pending_sent),
num_to_send)?

if len == 0 then
_apply_backpressure()
else
_pending_sent = _pending_sent + len
num_to_send,
addressof count))
| _SocketResultOk => _pending_sent = _pending_sent + count
| _SocketResultRetry => _apply_backpressure()
| _SocketResultError => error
end
else
// Non-graceful shutdown on error.
Expand Down Expand Up @@ -722,15 +721,16 @@ actor TCPConnection is AsioEventNotify
_pending_writev_windows .> push((data.size(), data.cpointer()))
_pending_writev_total = _pending_writev_total + data.size()

// Write as much data as possible
// Returns how many we sent or 0 if we are experiencing backpressure
let len = @pony_os_writev(_event,
_pending_writev_windows.cpointer(_pending_sent), I32(1))?

if len == 0 then
_apply_backpressure()
else
_pending_sent = _pending_sent + len
// Write as much data as possible.
var count: USize = 0
match \exhaustive\ _SocketResultDecoder(
@pony_os_writev(_event,
_pending_writev_windows.cpointer(_pending_sent),
I32(1),
addressof count))
| _SocketResultOk => _pending_sent = _pending_sent + count
| _SocketResultRetry => _apply_backpressure()
| _SocketResultError => error
end
else
// Non-graceful shutdown on error.
Expand Down Expand Up @@ -813,14 +813,19 @@ actor TCPConnection is AsioEventNotify
end

// Write as much data as possible.
var len = @pony_os_writev(_event,
_pending_writev_posix.cpointer(), num_to_send.i32()) ?

if _manage_pending_buffer(len, bytes_to_send, num_to_send)? then
return true
var count: USize = 0
match \exhaustive\ _SocketResultDecoder(
@pony_os_writev(_event,
_pending_writev_posix.cpointer(), num_to_send.i32(),
addressof count))
| _SocketResultOk =>
if _manage_pending_buffer(count, bytes_to_send, num_to_send)? then
return true
end
bytes_sent = bytes_sent + count
| _SocketResultRetry => _apply_backpressure()
| _SocketResultError => error
end

bytes_sent = bytes_sent + len
else
// Non-graceful shutdown on error.
hard_close()
Expand Down Expand Up @@ -949,13 +954,24 @@ actor TCPConnection is AsioEventNotify
Begin an IOCP read on Windows.
"""
ifdef windows then
try
// `count` is unused on this path: Windows IOCP `pony_os_recv`
// returns OK with count=0 because the actual byte count arrives
// asynchronously via `_complete_reads`. The local is required by
// the FFI shape.
//
// `_SocketResultRetry` is unreachable here — `iocp_recv` only
// distinguishes "queued" from "failed" — but `\exhaustive\`
// requires the arm. Treat it as failure to be safe.
var count: USize = 0
match \exhaustive\ _SocketResultDecoder(
@pony_os_recv(
_event,
_read_buf.cpointer(_read_buf_offset),
_read_buf.size() - _read_buf_offset) ?
else
hard_close()
_read_buf.size() - _read_buf_offset,
addressof count))
| _SocketResultOk => None
| _SocketResultRetry => hard_close()
| _SocketResultError => hard_close()
end
end

Expand Down Expand Up @@ -1011,12 +1027,17 @@ actor TCPConnection is AsioEventNotify
_read_buf_size()

// Read as much data as possible.
let len = @pony_os_recv(
_event,
_read_buf.cpointer(_read_buf_offset),
_read_buf.size() - _read_buf_offset) ?

if len == 0 then
var count: USize = 0
match \exhaustive\ _SocketResultDecoder(
@pony_os_recv(
_event,
_read_buf.cpointer(_read_buf_offset),
_read_buf.size() - _read_buf_offset,
addressof count))
| _SocketResultOk =>
_read_buf_offset = _read_buf_offset + count
sum = sum + count
| _SocketResultRetry =>
// Would block, try again later.
// this is safe because asio thread isn't currently subscribed
// for a read event so will not be writing to the readable flag
Expand All @@ -1025,13 +1046,13 @@ actor TCPConnection is AsioEventNotify
_reading = false
@pony_asio_event_resubscribe_read(_event)
return
| _SocketResultError => error
end

_read_buf_offset = _read_buf_offset + len
sum = sum + len
end
else
// The socket has been closed from the other side.
// The recv loop above raised — either an errno failure or a
// peer-closed condition (POSIX recv returning 0). Both surface
// through `_SocketResultError` and land here.
_shutdown_peer = true
hard_close()
end
Expand Down
Loading
Loading