From ccd9d91e0501db4c3b83d9927135c10c959fb976 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Sat, 5 Sep 2026 14:08:32 +0900 Subject: [PATCH 1/2] Release 1.0.6: pin wirelog to v0.60.0 Move the bundled and validated wirelog ref from v0.54.0 to v0.60.0 at peeled SHA 300f3e5150095c85331b561f1f42d99c27b4746f and bump PyreWire to 1.0.6. Keep the runtime minimum at 0.52.0. wirelog 0.60.0 adds 19 exported symbols and removes none, and the SONAME is unchanged, so no PyreWire code stops supporting 0.52.0. None of the new symbols is wrapped. The bump carries one compatibility break, and it is an engine-level one that reaches every PyreWire entry point: wirelog#1021 refuses a recursive min()/max() aggregate that shares an SCC with any other relation. optimize() still succeeds and evaluate() raises InvalidIRError. Programs that ran on 0.54.0 can stop running here. The refusal is right even though the rule is coarse. Such a consumer reads the aggregate's per-iteration content, so what it observes is decided by the evaluation strategy rather than by the program. Measured against both builds: the shape now covered by test_recursive_aggregate_sharing_an_scc_is_refused 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 workaround is to break the feedback edge so the consumer lands in a later stratum; upstream tracks narrowing the rule as wirelog#1135. wirelog's 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 with no message naming the relations involved. WL_LOG=EVAL:1 gets the engine's diagnostic. Positive `side` compound patterns now work in any body-atom position (wirelog#994), so conjunction order no longer changes the result. The remaining engine fixes in the range - wirelog#1075, #1083 and #1074 - are all unreachable through PyreWire's surface. Validated by building v0.60.0 from source and running the suite against it: 595 passed, 3 skipped, no new failures. The three tests gated on 0.54.0 behavior still pass. Claude-Session: https://claude.ai/code/session_01MqHwvCimcvrg1osEY5MH7o --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 66 +++++++++++++++++++++++++- docs/support.md | 4 +- docs/versioning.md | 1 + pyproject.toml | 10 ++-- src/pyrewire/__init__.py | 2 +- tests/docs/test_support_matrix.py | 4 +- tests/docs/test_versioning_contract.py | 15 +++++- tests/test_batch.py | 38 ++++++++++++++- tests/test_changelog_format.py | 6 ++- tests/test_ci_workflow.py | 2 +- tests/test_release_metadata.py | 6 +-- 12 files changed, 137 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec79997..38b0bb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f41b05..3b5aeab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,69 @@ 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 +- 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 @@ -227,7 +290,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 diff --git a/docs/support.md b/docs/support.md index 98f3e87..e4fcdc8 100644 --- a/docs/support.md +++ b/docs/support.md @@ -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 diff --git a/docs/versioning.md b/docs/versioning.md index 541310c..320cc75 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -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). diff --git a/pyproject.toml b/pyproject.toml index 3b8194b..b6f8efb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -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" @@ -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 @@ -96,7 +96,7 @@ 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}" @@ -104,7 +104,7 @@ repair-wheel-command = "DYLD_LIBRARY_PATH=$HOME/wirelog-install/lib delocate-whe [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}" diff --git a/src/pyrewire/__init__.py b/src/pyrewire/__init__.py index c12f544..d7b02d3 100644 --- a/src/pyrewire/__init__.py +++ b/src/pyrewire/__init__.py @@ -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" diff --git a/tests/docs/test_support_matrix.py b/tests/docs/test_support_matrix.py index 768741c..5f22b0a 100644 --- a/tests/docs/test_support_matrix.py +++ b/tests/docs/test_support_matrix.py @@ -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"] @@ -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 diff --git a/tests/docs/test_versioning_contract.py b/tests/docs/test_versioning_contract.py index 5db12af..4090766 100644 --- a/tests/docs/test_versioning_contract.py +++ b/tests/docs/test_versioning_contract.py @@ -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: @@ -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") diff --git a/tests/test_batch.py b/tests/test_batch.py index 3da0724..8dd0280 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -8,7 +8,7 @@ import pytest -from pyrewire._core.errors import ExecError +from pyrewire._core.errors import ExecError, InvalidIRError from pyrewire._ffi._loader import _parse_version, _pep440_base from pyrewire._ffi._util import wirelog_version from pyrewire.batch import BatchProgram, Result @@ -119,6 +119,42 @@ def test_optimize_preserves_head_bindings_with_four_body_atoms(): assert optimized == [(10, 1, 7)] +_RECURSIVE_AGG_IN_SCC = """ +.decl Edge(a: int32, b: int32) +.decl Label(x: int32, l: int32) +.decl Big(x: int32) +Edge(1,2). Edge(2,3). Edge(3,4). +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). +""" + + +@pytest.mark.skipif( + _wirelog_older_than((0, 60, 0)), + reason=( + "wirelog#1021 refuses this shape from 0.60.0 on; through 0.54.0 the " + "same program evaluates to a configuration-dependent answer." + ), +) +def test_recursive_aggregate_sharing_an_scc_is_refused(): + """A recursive `min()`/`max()` in an SCC with another relation is + refused at plan generation from wirelog 0.60.0 on (wirelog#1021). + + Through 0.54.0 this program returned `Label` all-1 while still + reporting `Big(3)` and `Big(4)` -- two nodes said to carry a label + above 2 when no surviving label exceeds 1. The refusal reaches + PyreWire as `InvalidIRError` from `evaluate()`; `optimize()` still + succeeds, because the rejection happens in plan generation. + """ + with BatchProgram.from_string(_RECURSIVE_AGG_IN_SCC) as bp: + bp.optimize() + with pytest.raises(InvalidIRError): + bp.evaluate() + + # ---------------------------------------------------------------------- # Result (#18) # ---------------------------------------------------------------------- diff --git a/tests/test_changelog_format.py b/tests/test_changelog_format.py index b604d5b..db7ca52 100644 --- a/tests/test_changelog_format.py +++ b/tests/test_changelog_format.py @@ -168,7 +168,11 @@ def test_release_compare_links_are_tag_to_tag(): changelog = (_repo_root() / "CHANGELOG.md").read_text(encoding="utf-8") assert ( - "[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...HEAD" + "[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.6...HEAD" + in changelog + ) + assert ( + "[1.0.6]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...v1.0.6" in changelog ) assert ( diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py index 0fd25de..9c548ee 100644 --- a/tests/test_ci_workflow.py +++ b/tests/test_ci_workflow.py @@ -24,7 +24,7 @@ def _workflow() -> dict[str, Any]: def test_ci_default_wirelog_version_is_exact_sha(): env = _workflow()["jobs"]["test"]["env"] default = str(env["WIRELOG_VERSION"]) - assert "9f80877c82564cb92ea45bd6fffc2d681b0e13de" in default + assert "300f3e5150095c85331b561f1f42d99c27b4746f" in default def test_ci_matrix_drops_python_310(): diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 467141a..a04072b 100644 --- a/tests/test_release_metadata.py +++ b/tests/test_release_metadata.py @@ -28,9 +28,9 @@ def _runtime_version_literal() -> str: return match.group(1) -def test_project_and_runtime_versions_are_105(): - assert _pyproject()["project"]["version"] == "1.0.5" - assert _runtime_version_literal() == "1.0.5" +def test_project_and_runtime_versions_are_106(): + assert _pyproject()["project"]["version"] == "1.0.6" + assert _runtime_version_literal() == "1.0.6" def test_project_classifiers_mark_stable_python_311_through_314(): From 4366021a24647c7b89abf4f1a66e47377b6d4838 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Sat, 5 Sep 2026 14:08:45 +0900 Subject: [PATCH 2/2] Add examples/14_arithmetic_operations.py, a port of wirelog example 14 wirelog 0.60.0 brings float support, and nothing in PyreWire exercised it. 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 any of it, because a float column decodes as a Python float through the existing result path. The same release added arithmetic expressions in a rule head and multi-line .decl continuations. The example ports wirelog examples/14-arithmetic-operations and asserts that project's own golden output: the three result rows, min/max over the int64 column, average_value 2.5, count 3, and the single zero_observed row. Two behaviors are demonstrated because they are easy to misread. Arithmetic parses left-associatively, so A + B * C means (A + B) * C and the precedence row derives 22 rather than 14. And the typed float ingress canonicalizes -0.0 and +0.0 to the same +0.0, so two zero_input facts collapse to one row - the test asserts the surviving row's sign bit, not just its value, since 0.0 == -0.0 in Python. Float values reach a program through its source text only, which is why the example uses BatchProgram with inline facts rather than a session: EasySession.insert() 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. The whole program - both halves, not just the float one - is a parse error on 0.54.0 and older, so the test skips below 0.60.0 rather than partially running, and the example is left out of the ungated parametrize list that the other ports share. Claude-Session: https://claude.ai/code/session_01MqHwvCimcvrg1osEY5MH7o --- CHANGELOG.md | 23 +++++ examples/14_arithmetic_operations.py | 140 +++++++++++++++++++++++++++ examples/README.md | 1 + tests/integration/test_examples.py | 32 ++++++ 4 files changed, 196 insertions(+) create mode 100644 examples/14_arithmetic_operations.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5aeab..8a8bb52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,29 @@ wirelog floor and a validated wirelog ref (see 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. diff --git a/examples/14_arithmetic_operations.py b/examples/14_arithmetic_operations.py new file mode 100644 index 0000000..efe2987 --- /dev/null +++ b/examples/14_arithmetic_operations.py @@ -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") diff --git a/examples/README.md b/examples/README.md index 353d18a..1b22cd0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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()` | diff --git a/tests/integration/test_examples.py b/tests/integration/test_examples.py index 372a147..37db03d 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -11,6 +11,7 @@ from __future__ import annotations import importlib +import math import sys from pathlib import Path from typing import Any @@ -107,6 +108,37 @@ def test_crc32_checksum_example_partitions_frames_by_stored_checksum() -> None: } +def test_arithmetic_operations_example_matches_wirelog_golden_output() -> None: + """Golden output for wirelog `examples/14-arithmetic-operations`. + + Arithmetic in a rule head and the `float` column `average()` needs + both arrived in wirelog 0.60.0; the whole program is a parse error + on 0.54.0 and older, so the example is skipped there rather than + partially run. + """ + if _wirelog_ver() < (0, 60, 0): + pytest.skip("arithmetic heads and float columns need wirelog 0.60.0 or newer") + + mod = _import_example("14_arithmetic_operations") + out = mod.run() + + assert out["result"] == [ + ("negative", -12, -22, -85, -3, -2, -24), + ("positive", 22, 12, 85, 3, 2, 44), + ("precedence", 11, 5, 24, 2, 2, 22), + ] + assert out["minimum"] == [(-17,)] + assert out["maximum"] == [(17,)] + assert out["average_value"] == [(2.5,)] + assert out["sample_count"] == [(3,)] + + # `-0.0` and `+0.0` canonicalize to one row carrying `+0.0`. + assert len(out["zero_observed"]) == 1 + (zero,) = out["zero_observed"][0] + assert zero == 0.0 + assert math.copysign(1.0, zero) > 0 + + def _wirelog_ver() -> tuple[int, ...]: try: import pyrewire