|
| 1 | +## Fix use-after-free in IOCP ASIO system |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## Remove support for Alpine 3.20 |
| 8 | + |
| 9 | +Alpine 3.20 has reached end-of-life. We no longer test against it or build ponyc releases for it. |
| 10 | + |
| 11 | +## Fix with tuple only processing first binding in build_with_dispose |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +For example, the following code compiled without error even though `_` is not allowed in a `with` block: |
| 16 | + |
| 17 | +```pony |
| 18 | +class D |
| 19 | + new create() => None |
| 20 | + fun dispose() => None |
| 21 | +
|
| 22 | +actor Main |
| 23 | + new create(env: Env) => |
| 24 | + with (a, _) = (D.create(), D.create()) do |
| 25 | + None |
| 26 | + end |
| 27 | +``` |
| 28 | + |
| 29 | +This now correctly produces an error: `_ isn't allowed for a variable in a with block`. |
| 30 | + |
| 31 | +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. |
| 32 | + |
| 33 | +## Fix memory leak in Windows networking subsystem |
| 34 | + |
| 35 | +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. |
| 36 | + |
| 37 | +## Remove docgen pass |
| 38 | + |
| 39 | +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. |
| 40 | + |
| 41 | +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`. |
| 42 | + |
| 43 | +## Fix spurious error when assigning to a field on an `as` cast in a try block |
| 44 | + |
| 45 | +Assigning to a field on the result of an `as` expression inside a `try` block incorrectly produced an error about consumed identifiers: |
| 46 | + |
| 47 | +```pony |
| 48 | +class Wumpus |
| 49 | + var hunger: USize = 0 |
| 50 | +
|
| 51 | +actor Main |
| 52 | + new create(env: Env) => |
| 53 | + let a: (Wumpus | None) = Wumpus |
| 54 | + try |
| 55 | + (a as Wumpus).hunger = 1 |
| 56 | + end |
| 57 | +``` |
| 58 | + |
| 59 | +``` |
| 60 | +can't reassign to a consumed identifier in a try expression if there is a |
| 61 | +partial call involved |
| 62 | +``` |
| 63 | + |
| 64 | +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`). |
| 65 | + |
| 66 | +## Fix segfault when using Generator.map with PonyCheck shrinking |
| 67 | + |
| 68 | +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: |
| 69 | + |
| 70 | +```pony |
| 71 | +let gen = recover val |
| 72 | + Generators.u32().map[String]({(n: U32): String^ => n.string()}) |
| 73 | +end |
| 74 | +PonyCheck.for_all[String](gen, h)( |
| 75 | + {(sample: String, ph: PropertyHelper) => |
| 76 | + ph.assert_true(sample.size() > 0) |
| 77 | + })? |
| 78 | +``` |
| 79 | + |
| 80 | +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. |
| 81 | + |
| 82 | +## Add --shuffle option to PonyTest |
| 83 | + |
| 84 | +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. |
| 85 | + |
| 86 | +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: |
| 87 | + |
| 88 | +``` |
| 89 | +Test seed: 8675309 |
| 90 | +``` |
| 91 | + |
| 92 | +Grab that seed from your CI log and pass it back to reproduce the exact ordering: |
| 93 | + |
| 94 | +``` |
| 95 | +./my-tests --shuffle=8675309 |
| 96 | +``` |
| 97 | + |
| 98 | +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. |
| 99 | + |
| 100 | +`--list --shuffle=SEED` shows the test names in the order that seed would produce, so you can preview orderings without running anything. |
| 101 | + |
| 102 | +## Fix pony-lint blank-lines rule false positives on multi-line docstrings |
| 103 | + |
| 104 | +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. |
| 105 | + |
| 106 | +## Fix `FloatingPoint.frexp` returning unsigned exponent |
| 107 | + |
| 108 | +`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. |
| 109 | + |
| 110 | +The return type is now `(A, I32)` instead of `(A, U32)`. If you destructure the result and type the exponent, update it: |
| 111 | + |
| 112 | +```pony |
| 113 | +// Before |
| 114 | +(let mantissa, let exp: U32) = my_float.frexp() |
| 115 | +
|
| 116 | +// After |
| 117 | +(let mantissa, let exp: I32) = my_float.frexp() |
| 118 | +``` |
| 119 | + |
| 120 | +## Fix asymmetric NaN handling in F32/F64 min and max |
| 121 | + |
| 122 | +`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. |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | +## Use LLVM intrinsics for NaN-propagating float min and max |
| 127 | + |
| 128 | +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. |
| 129 | + |
| 130 | +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. |
| 131 | + |
| 132 | +Before: |
| 133 | + |
| 134 | +```pony |
| 135 | +// Old behavior: result depended on argument order |
| 136 | +F32.nan().min(F32(5.0)) // => 5.0 |
| 137 | +F32(5.0).min(F32.nan()) // => NaN |
| 138 | +``` |
| 139 | + |
| 140 | +After: |
| 141 | + |
| 142 | +```pony |
| 143 | +// New behavior: NaN propagates regardless of position |
| 144 | +F32.nan().min(F32(5.0)) // => NaN |
| 145 | +F32(5.0).min(F32.nan()) // => NaN |
| 146 | +``` |
| 147 | + |
0 commit comments