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..8a8bb52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 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/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/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/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 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():