Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ jobs:
python: ["3.11", "3.12", "3.13", "3.14"]
env:
# WIRELOG_VERSION here is the exact wirelog ref CI builds against.
# The default is pinned to the wirelog v0.54.0 release commit;
# The default is pinned to the wirelog v0.60.0 release commit;
# override the repository variable to test another ref before
# updating this fallback.
WIRELOG_VERSION: ${{ vars.WIRELOG_VERSION || '9f80877c82564cb92ea45bd6fffc2d681b0e13de' }}
WIRELOG_VERSION: ${{ vars.WIRELOG_VERSION || '300f3e5150095c85331b561f1f42d99c27b4746f' }}
# `runner.temp` is NOT available in job-level `env:` (only in
# step-level contexts) — using it here caused the whole workflow
# to fail at startup (#114). `github.workspace` IS resolvable at
Expand Down
89 changes: 88 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,92 @@ wirelog floor and a validated wirelog ref (see

## [Unreleased]

## [1.0.6] - 2026-09-05

### Changed
- The bundled and validated wirelog ref moves from `v0.54.0` to
`v0.60.0` at peeled SHA
`300f3e5150095c85331b561f1f42d99c27b4746f`.
- The minimum compatible runtime wirelog version remains `0.52.0`.
wirelog 0.60.0 adds 19 exported symbols and removes none, and the
library SONAME is unchanged, so no PyreWire code stops supporting
`0.52.0`. The PyreWire public API is unchanged; none of the new
symbols is wrapped yet.
- **A recursive `min()`/`max()` aggregate that shares an SCC with any
other relation is now refused** (wirelog#1021). This is an
engine-level compatibility break that reaches every PyreWire entry
point: `optimize()` still succeeds, and `evaluate()` raises
`InvalidIRError`. Programs that ran on 0.54.0 can stop running here.

The refusal is deliberate. Such a consumer reads the aggregate's
*per-iteration* content -- each round's per-rule output, before that
round's cross-rule domination -- so what it observes is decided by the
engine's evaluation strategy rather than by the program. The shape

Label(x, min(x)) :- Edge(x, y).
Label(y, min(y)) :- Edge(x, y).
Label(x, min(l)) :- Label(y, l), Edge(y, x).
Big(x) :- Label(x, l), l > 2.
Label(x, min(9)) :- Big(x).

over `Edge(1,2) (2,3) (3,4)` returned `Label` all-1 on 0.54.0 while
still reporting `Big(3)` and `Big(4)` -- two nodes said to carry a
label above 2 when no surviving label exceeds 1.

The rule is coarse, and upstream says so: programs that answer
correctly today are refused too. The workaround is to break the
feedback edge so the consumer lands in a later stratum -- dropping
`Label(x, min(9)) :- Big(x).` above yields `Label` all-1 with an empty
`Big`. Narrowing the rule is tracked as wirelog#1135.

wirelog's own rejection is silent at the default log level, and
`wl_plan_from_program()` has no error channel (wirelog#1137), so
PyreWire can only surface the generic `InvalidIRError`; it has no
message naming the relations involved. Set `WL_LOG=EVAL:1` in the
environment to get the engine's diagnostic on stderr.
- **Positive `side` compound patterns now work in any body-atom
position** (wirelog#994). Their generated side-relation joins are
spliced onto the left-deep body chain, so conjunction order no longer
changes the result. A program that compensated for the old
order-dependence may need its body order revisited.

### Added
- **Float columns, float literals and the `average()` aggregate now
work**, delivered by the engine bump. A `.decl` may declare a `float`
column, a fact may carry a float literal, and `average()` over a
`float` operand returns a float. PyreWire needed no change to carry
them: a `float` column decodes as a Python `float` through the
existing result path. `-0.0` and `+0.0` canonicalize to the same
`+0.0` value on ingress, so they collapse to one row.

Float values reach a program through its **source text** only.
`EasySession.insert()` still carries `int64` lanes and raises
`ExecError` on a Python `float`; wirelog 0.60.0 adds
`wirelog_session_insert_typed()` for typed ingress, and PyreWire does
not wrap it yet.
- **Arithmetic expressions in a rule head** (`A + B`, `A - B`, `A * B`,
`A / B`, `A % B`) and multi-line `.decl` continuations, also from the
bump. Arithmetic parses left-associatively: `A + B * C` means
`(A + B) * C`, not the conventional multiplication-first grouping.
Division truncates toward zero and the remainder keeps the dividend's
sign.
- `examples/14_arithmetic_operations.py`, a port of wirelog
`examples/14-arithmetic-operations`, covering both of the above
against wirelog's own golden output. It is skipped on engines older
than 0.60.0, where the whole program is a parse error.
- wirelog 0.60.0 exposes `wirelog_program_relation_has_input()`, which
reports whether a relation carries a parsed `.input` directive without
opening its source (wirelog#1070). PyreWire does not wrap it yet.

### Fixed
- Engine fixes picked up by the bump, none of them reachable through
PyreWire's surface: a partial NULL guard in the columnar evaluator's
post-eval skip (wirelog#1075, unreachable without editing library
code), out-of-range dependency-graph edges corrupting SCC detection
(wirelog#1083, reachable only by an embedder building the graph by
hand), and arrangement row counts overflowing their hash-table sizes
(wirelog#1074, needs more than 2^30 rows in one arrangement).

## [1.0.5] - 2026-08-11

### Fixed
Expand Down Expand Up @@ -227,7 +313,8 @@ runtime wirelog version remaining `0.44.0`.
wirelog#852. They are available in the later [1.0.0] line, whose
validated wirelog ref is v0.50.0. Tracked in wirelog#859.

[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...HEAD
[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.6...HEAD
[1.0.6]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.3...v1.0.4
[1.0.3]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.2...v1.0.3
Expand Down
4 changes: 2 additions & 2 deletions docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ not need to install wirelog separately.
| macOS | `arm64` | `macos-15` | Apple Silicon only for v1.0; no macOS Intel or universal2 wheel is produced. |
| Windows | `win_amd64` / `AMD64` | `windows-2025-vs2026` | Built with MSVC and repaired with delvewheel. |

The bundled library is built from wirelog v0.54.0, using peeled SHA
`9f80877c82564cb92ea45bd6fffc2d681b0e13de`.
The bundled library is built from wirelog v0.60.0, using peeled SHA
`300f3e5150095c85331b561f1f42d99c27b4746f`.

## Source Distributions

Expand Down
1 change: 1 addition & 0 deletions docs/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ release to publish; it is **not** tied to the wirelog change.
| `1.0.3` | `0.52.0` | `da82a14a7e1472e33aa6ed753b3bc3dfe28a68ba` | Validated against wirelog `v0.52.0` (peeled tag SHA); runtime minimum raised to `0.52.0`. Bundled engine bumped to v0.52.0 and the loader floor moved up to match. |
| `1.0.4` | `0.52.0` | `668f82ad69c2bbfc8e8111839302adf1360f55da` | Validated against wirelog `v0.53.0` (peeled tag SHA); runtime minimum remains `0.52.0`. Bundled engine bumped to v0.53.0 with no public C header or SONAME change. |
| `1.0.5` | `0.52.0` | `9f80877c82564cb92ea45bd6fffc2d681b0e13de` | Validated against wirelog `v0.54.0` (peeled tag SHA); runtime minimum remains `0.52.0`. Bundled engine bumped to v0.54.0 to pick up the wirelog#955 semijoin layout fix (#180); the public C header change is additive and the SONAME is unchanged. |
| `1.0.6` | `0.52.0` | `300f3e5150095c85331b561f1f42d99c27b4746f` | Validated against wirelog `v0.60.0` (peeled tag SHA); runtime minimum remains `0.52.0`. Bundled engine bumped to v0.60.0; the exported ABI is additive (19 new symbols, none removed) and the SONAME is unchanged. wirelog#1021 refuses a recursive `min()`/`max()` that shares an SCC with another relation - an engine-level compatibility break that reaches any program PyreWire runs. |

The table grows with every release; the source of truth is the
[CHANGELOG](https://github.com/semantic-reasoning/PyreWire/blob/main/CHANGELOG.md).
140 changes: 140 additions & 0 deletions examples/14_arithmetic_operations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
"""Port of wirelog `examples/14-arithmetic-operations` to PyreWire.

Two programs. The first evaluates arithmetic expressions in a rule head
over `int64` columns. The second runs the `min` / `max` / `average` /
`count` aggregates, where `average` requires a declared `float` operand.

Requires wirelog >= 0.60.0. Both halves are parse errors on 0.54.0 and
older: arithmetic expressions in a rule head, multi-line `.decl`
continuations, `float` columns, float literals and `average()` all
arrived together. PyreWire needed no change to carry them — a `float`
column decodes as a Python `float` through the existing result path.

Float values reach a program through its source text only.
`EasySession.insert()` carries `int64` lanes and raises `ExecError` on a
Python `float`; wirelog's typed-ingress entry point
(`wirelog_session_insert_typed`) is not wrapped yet, which is why this
example uses `BatchProgram` with inline facts rather than a session.

Two behaviors are easy to misread and are demonstrated deliberately:

- Arithmetic parses left-associatively. `A + B * C` means `(A + B) * C`,
not the conventional multiplication-first grouping, so the
`precedence` row derives `22` rather than `14`. Use explicit
intermediate relations when conventional grouping is needed.
- The typed float ingress canonicalizes `-0.0` and `+0.0` to the same
`+0.0` value, so the two `zero_input` facts collapse to one row.

Division truncates toward zero (`-17 / 5` is `-3`) and the remainder
keeps the dividend's sign (`-17 % 5` is `-2`). The rule filters out rows
with `B == 0` rather than relying on a default for division by zero.
"""

from __future__ import annotations

import math

from pyrewire import BatchProgram, wirelog_version

ARITHMETIC_SRC = """
.decl sample(label: symbol, a: int64, b: int64, c: int64)
.decl result(label: symbol, added: int64, difference: int64,
product: int64, quotient: int64, remainder: int64,
precedence: int64)

sample("negative", -17, 5, 2).
sample("positive", 17, 5, 2).
sample("precedence", 8, 3, 2).

result(Label, A + B, A - B, A * B, A / B, A % B, A + B * C)
:- sample(Label, A, B, C), B != 0.
"""

AGGREGATE_SRC = """
.decl sample(a: int64, value: float)
.decl zero_input(value: float)
.decl zero_observed(value: float)
.decl minimum(value: int64)
.decl maximum(value: int64)
.decl average_value(value: float)
.decl sample_count(value: int64)

sample(-17, 1.5).
sample(17, 2.5).
sample(8, 3.5).
zero_input(-0.0).
zero_input(0.0).

minimum(min(A)) :- sample(A, _).
maximum(max(A)) :- sample(A, _).
average_value(average(Value)) :- sample(_, Value).
sample_count(count(A)) :- sample(A, _).
zero_observed(Value) :- zero_input(Value).
"""

AGGREGATE_RELATIONS = (
"minimum",
"maximum",
"average_value",
"sample_count",
"zero_observed",
)

# `symbol` columns come back as interned ids, not the source spelling, so
# the example resolves the label itself. `added` (`A + B`) is unique per
# row, which makes it a stable key. Example 05 decodes symbols the same
# way.
_LABEL_BY_ADDED = {-12: "negative", 22: "positive", 11: "precedence"}

MINIMUM_WIRELOG = (0, 60, 0)


def wirelog_supports_float() -> bool:
"""Whether the loaded engine is new enough to run this example."""
parts = tuple(int(part) for part in wirelog_version().split(".")[:3])
return parts >= MINIMUM_WIRELOG


def _run_arithmetic() -> list[tuple]:
with BatchProgram.from_string(ARITHMETIC_SRC) as bp:
bp.optimize()
res = bp.evaluate()
try:
return sorted((_LABEL_BY_ADDED[row[1]], *row[1:]) for row in res.relation("result"))
finally:
res.close()


def _run_aggregates() -> dict[str, list[tuple]]:
with BatchProgram.from_string(AGGREGATE_SRC) as bp:
bp.optimize()
res = bp.evaluate()
try:
return {name: res.relation(name) for name in AGGREGATE_RELATIONS}
finally:
res.close()


def run() -> dict[str, list[tuple]]:
out: dict[str, list[tuple]] = {"result": _run_arithmetic()}
out.update(_run_aggregates())
return out


if __name__ == "__main__": # pragma: no cover
if not wirelog_supports_float():
raise SystemExit(
f"wirelog {wirelog_version()} is too old for this example; "
f"{'.'.join(str(p) for p in MINIMUM_WIRELOG)} or newer is required"
)

results = run()
for relation, rows in results.items():
print(f"== {relation} ==")
for row in rows:
print(row)

(zero,) = results["zero_observed"][0]
sign = "+" if math.copysign(1.0, zero) > 0 else "-"
print(f"\n-0.0 and +0.0 both ingressed as {sign}0.0")
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ test under `tests/integration/test_examples.py`.
| `11_time_evolution.py` | wirelog 11 | Per-epoch delta isolation: each `step()` is a discrete time slice |
| `12_batch_vs_session.py` | wirelog 12 (in spirit) | Compares the batch closure path with the session's EDB preview |
| `12_snapshot_vs_delta.py` | wirelog 12 | Side-by-side comparison of `snapshot()` vs `step()` deliveries |
| `14_arithmetic_operations.py` | wirelog 14 | Arithmetic expressions in a rule head, and the `min` / `max` / `average` / `count` aggregates over a `float` column (needs wirelog >= 0.60.0) |
| `csv_adapter_reachability.py` | PyreWire IO adapter | Supplies `.input` facts through `register_adapter` and `load_input_files()` |
| `retraction_basics.py` | wirelog 09 | Symmetric retraction through `step()` |
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pyrewire"
version = "1.0.5"
version = "1.0.6"
description = "Python wrapper for wirelog - declarative dataflow analysis"
readme = "README.md"
requires-python = ">=3.11"
Expand Down Expand Up @@ -74,7 +74,7 @@ test-requires = ["pytest", "pytest-cov"]
before-build = "python {project}/scripts/bundle_libwirelog.py"
# Pin the wirelog source ref here too so the environment matches the
# `WIRELOG_VERSION` used by the test-matrix workflow.
environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de" }
environment = { WIRELOG_VERSION = "300f3e5150095c85331b561f1f42d99c27b4746f" }

[tool.cibuildwheel.linux]
manylinux-x86_64-image = "manylinux_2_28"
Expand All @@ -83,7 +83,7 @@ before-all = """
pip install meson && \
WIRELOG_PREFIX=/wirelog-install bash {project}/scripts/build_wirelog.sh
"""
environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de", WIRELOG_PREFIX = "/wirelog-install", WIRELOG_LIB = "/wirelog-install/lib/libwirelog.so.1", LD_LIBRARY_PATH = "/wirelog-install/lib" }
environment = { WIRELOG_VERSION = "300f3e5150095c85331b561f1f42d99c27b4746f", WIRELOG_PREFIX = "/wirelog-install", WIRELOG_LIB = "/wirelog-install/lib/libwirelog.so.1", LD_LIBRARY_PATH = "/wirelog-install/lib" }
# `auditwheel` bundles libwirelog.so.1 into the wheel and patches the
# RPATH (#31). Without this every wheel would ship as a manylinux
# wheel that immediately fails at import time on systems without the
Expand All @@ -96,15 +96,15 @@ before-all = """
brew install ninja meson pkg-config && \
WIRELOG_PREFIX=$HOME/wirelog-install bash {project}/scripts/build_wirelog.sh
"""
environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de", WIRELOG_PREFIX = "$HOME/wirelog-install", WIRELOG_LIB = "$HOME/wirelog-install/lib/libwirelog.1.dylib", DYLD_LIBRARY_PATH = "$HOME/wirelog-install/lib" }
environment = { WIRELOG_VERSION = "300f3e5150095c85331b561f1f42d99c27b4746f", WIRELOG_PREFIX = "$HOME/wirelog-install", WIRELOG_LIB = "$HOME/wirelog-install/lib/libwirelog.1.dylib", DYLD_LIBRARY_PATH = "$HOME/wirelog-install/lib" }
# `delocate` is macOS's auditwheel equivalent — copies the dylib into
# the wheel and rewrites install names so the bundled copy wins.
repair-wheel-command = "DYLD_LIBRARY_PATH=$HOME/wirelog-install/lib delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"

[tool.cibuildwheel.windows]
archs = ["AMD64"]
before-all = "powershell {project}\\scripts\\build_wirelog.ps1"
environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de", WIRELOG_PREFIX = "C:/wirelog-install", WIRELOG_LIB = "C:/wirelog-install/bin/wirelog-1.dll" }
environment = { WIRELOG_VERSION = "300f3e5150095c85331b561f1f42d99c27b4746f", WIRELOG_PREFIX = "C:/wirelog-install", WIRELOG_LIB = "C:/wirelog-install/bin/wirelog-1.dll" }
# `delvewheel` is the Windows analogue. The bundled DLL ends up in
# `pyrewire/_lib/` and the loader (#2) finds it ahead of system paths.
repair-wheel-command = "pip install delvewheel && delvewheel repair -w {dest_dir} --no-mangle-all --add-path C:/wirelog-install/bin {wheel}"
Expand Down
2 changes: 1 addition & 1 deletion src/pyrewire/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
"""PyreWire - Python wrapper for wirelog declarative dataflow analysis."""

__version__ = "1.0.5"
__version__ = "1.0.6"
__author__ = "PyreWire Contributors"
__license__ = "Apache-2.0 OR GPL-3.0-or-later"

Expand Down
4 changes: 2 additions & 2 deletions tests/docs/test_support_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

yaml = pytest.importorskip("yaml")

PINNED_WIRELOG_SHA = "9f80877c82564cb92ea45bd6fffc2d681b0e13de"
PINNED_WIRELOG_SHA = "300f3e5150095c85331b561f1f42d99c27b4746f"
SUPPORTED_PYTHONS = ["3.11", "3.12", "3.13", "3.14"]
SUPPORTED_CP_TAGS = ["cp311", "cp312", "cp313", "cp314"]
SUPPORTED_RUNNERS = ["ubuntu-24.04", "macos-15", "windows-2025-vs2026"]
Expand Down Expand Up @@ -122,7 +122,7 @@ def test_support_wirelog_bundle_contract_matches_config_and_versioning():
assert cibw["macos"]["environment"]["WIRELOG_VERSION"] == PINNED_WIRELOG_SHA
assert cibw["windows"]["environment"]["WIRELOG_VERSION"] == PINNED_WIRELOG_SHA

assert "wirelog v0.54.0" in support
assert "wirelog v0.60.0" in support
assert PINNED_WIRELOG_SHA in support
assert "peeled SHA" in support
assert "Wheels bundle" in versioning
Expand Down
15 changes: 13 additions & 2 deletions tests/docs/test_versioning_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
yaml = pytest.importorskip("yaml")

# The wirelog ref the current release bundles and validates against.
PINNED_WIRELOG_SHA = "9f80877c82564cb92ea45bd6fffc2d681b0e13de"
PINNED_WIRELOG_SHA = "300f3e5150095c85331b561f1f42d99c27b4746f"
# Historical pins kept frozen in the compatibility table, one per release.
WIRELOG_SHA_100 = "272edf3a24b25676f12c4b843d55510f5048dd2f"
WIRELOG_SHA_101 = "0c6e0cdaee7db069be5d8d896bb59bdcb15673e9"
WIRELOG_SHA_102 = WIRELOG_SHA_101
WIRELOG_SHA_103 = "da82a14a7e1472e33aa6ed753b3bc3dfe28a68ba"
WIRELOG_SHA_104 = "668f82ad69c2bbfc8e8111839302adf1360f55da"
WIRELOG_SHA_105 = PINNED_WIRELOG_SHA
WIRELOG_SHA_105 = "9f80877c82564cb92ea45bd6fffc2d681b0e13de"
WIRELOG_SHA_106 = PINNED_WIRELOG_SHA


def _repo_root() -> Path:
Expand Down Expand Up @@ -87,6 +88,16 @@ def test_versioning_documents_105_wirelog_pin_and_runtime_floor():
assert "peeled tag SHA" in notes


def test_versioning_documents_106_wirelog_pin_and_runtime_floor():
minimum, validated_ref, notes = _versioning_row("1.0.6")

assert minimum == "`0.52.0`"
assert validated_ref == f"`{WIRELOG_SHA_106}`"
assert "v0.60.0" in notes
assert "runtime minimum remains `0.52.0`" in notes
assert "peeled tag SHA" in notes


def test_versioning_explains_sdist_and_wheel_wirelog_behavior():
text = _read("docs/versioning.md")

Expand Down
Loading