diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7d8915..ec79997 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.53.0 release commit; + # The default is pinned to the wirelog v0.54.0 release commit; # override the repository variable to test another ref before # updating this fallback. - WIRELOG_VERSION: ${{ vars.WIRELOG_VERSION || '668f82ad69c2bbfc8e8111839302adf1360f55da' }} + WIRELOG_VERSION: ${{ vars.WIRELOG_VERSION || '9f80877c82564cb92ea45bd6fffc2d681b0e13de' }} # `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 6868796..4f41b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,49 @@ wirelog floor and a validated wirelog ref (see ## [Unreleased] +## [1.0.5] - 2026-08-11 + +### Fixed +- `BatchProgram.optimize()` no longer corrupts head bindings for a rule + with four or more body atoms (#180). The engine's SIP-inserted semijoin + widened the reported output layout by the right relation's arity, which + shifted every column resolved above it; the out-of-range lookup fell + back to column 0, so the last head variable came back as `0` while the + row count and arity stayed correct. Fixed upstream in wirelog#955 and + delivered here by the engine bump below. There was nothing to fix in + PyreWire — `optimize()` is a faithful passthrough — so a source install + resolving an older system `libwirelog` is still affected. +- `crc32_ethernet()` now agrees with externally computed CRCs. Through + wirelog 0.53.0 it returned a checksum that matched no stored value, so + `examples/05_crc32_checksum` classified every frame as corrupt. + +### Changed +- The bundled and validated wirelog ref moves from `v0.53.0` to + `v0.54.0` at peeled SHA + `9f80877c82564cb92ea45bd6fffc2d681b0e13de`. +- The minimum compatible runtime wirelog version remains `0.52.0`. + wirelog 0.54.0's public C header change is additive — one appended + `wirelog_str_fn_t` member and documentation — and the library SONAME is + unchanged, so no PyreWire code stops supporting `0.52.0`. The PyreWire + public API is unchanged. Tests covering behavior that only wirelog + `0.54.0` provides are skipped on older runtimes. +- `EasySession.insert()` now raises when the row is wider or narrower + than the relation's `.decl`, on the first insert as well as later ones + (wirelog#1038). Previously a relation's width was whatever its first + producer supplied: too narrow fabricated a zero column, too wide + dropped the surplus, both silently. +- Programs that are heavy on joins may evaluate more slowly. wirelog#955 + removes an under-derivation that the previous speed depended on, so + correct answers cost more than the wrong ones did — upstream measured + DOOP W=1 at ~94 s before and ~1,414 s after. +- `wirelog_program_get_facts`, `wirelog_io_ctx_num_cols`, and + `wirelog_io_ctx_col_type` report the *physical* row stride rather than + the declared column count. The two differ only for a relation declaring + an `inline` compound column. PyreWire passes all three through + unchanged, so its own contract is unaffected; an embedder that + reconstructed the stride from a schema of its own should read wirelog's + release notes. + ## [1.0.4] - 2026-07-31 ### Changed @@ -184,7 +227,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.4...HEAD +[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...HEAD +[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 [1.0.2]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.1...v1.0.2 diff --git a/docs/support.md b/docs/support.md index 21f8911..98f3e87 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.53.0, using peeled SHA -`668f82ad69c2bbfc8e8111839302adf1360f55da`. +The bundled library is built from wirelog v0.54.0, using peeled SHA +`9f80877c82564cb92ea45bd6fffc2d681b0e13de`. ## Source Distributions diff --git a/docs/versioning.md b/docs/versioning.md index c288e62..541310c 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -59,6 +59,7 @@ release to publish; it is **not** tied to the wirelog change. | `1.0.2` | `0.44.0` | `0c6e0cdaee7db069be5d8d896bb59bdcb15673e9` | Validated against wirelog `v0.51.0` (peeled tag SHA); runtime minimum remains `0.44.0`. PyreWire-only maintenance release (CI action bumps, SPDX headers, docs); no engine 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. | 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 42ce70d..3b8194b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pyrewire" -version = "1.0.4" +version = "1.0.5" 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 = "668f82ad69c2bbfc8e8111839302adf1360f55da" } +environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de" } [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 = "668f82ad69c2bbfc8e8111839302adf1360f55da", WIRELOG_PREFIX = "/wirelog-install", WIRELOG_LIB = "/wirelog-install/lib/libwirelog.so.1", LD_LIBRARY_PATH = "/wirelog-install/lib" } +environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de", 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 = "668f82ad69c2bbfc8e8111839302adf1360f55da", WIRELOG_PREFIX = "$HOME/wirelog-install", WIRELOG_LIB = "$HOME/wirelog-install/lib/libwirelog.1.dylib", DYLD_LIBRARY_PATH = "$HOME/wirelog-install/lib" } +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" } # `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 = "668f82ad69c2bbfc8e8111839302adf1360f55da", WIRELOG_PREFIX = "C:/wirelog-install", WIRELOG_LIB = "C:/wirelog-install/bin/wirelog-1.dll" } +environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de", 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 dcf6ee4..c12f544 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.4" +__version__ = "1.0.5" __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 3d97165..768741c 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 = "668f82ad69c2bbfc8e8111839302adf1360f55da" +PINNED_WIRELOG_SHA = "9f80877c82564cb92ea45bd6fffc2d681b0e13de" 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.53.0" in support + assert "wirelog v0.54.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 2c2f467..5db12af 100644 --- a/tests/docs/test_versioning_contract.py +++ b/tests/docs/test_versioning_contract.py @@ -12,13 +12,14 @@ yaml = pytest.importorskip("yaml") # The wirelog ref the current release bundles and validates against. -PINNED_WIRELOG_SHA = "668f82ad69c2bbfc8e8111839302adf1360f55da" +PINNED_WIRELOG_SHA = "9f80877c82564cb92ea45bd6fffc2d681b0e13de" # 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 = PINNED_WIRELOG_SHA +WIRELOG_SHA_104 = "668f82ad69c2bbfc8e8111839302adf1360f55da" +WIRELOG_SHA_105 = PINNED_WIRELOG_SHA def _repo_root() -> Path: @@ -76,6 +77,16 @@ def test_versioning_documents_104_wirelog_pin_and_runtime_floor(): assert "peeled tag SHA" in notes +def test_versioning_documents_105_wirelog_pin_and_runtime_floor(): + minimum, validated_ref, notes = _versioning_row("1.0.5") + + assert minimum == "`0.52.0`" + assert validated_ref == f"`{WIRELOG_SHA_105}`" + assert "v0.54.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 7a69c39..372a147 100644 --- a/tests/integration/test_examples.py +++ b/tests/integration/test_examples.py @@ -81,17 +81,29 @@ def test_hash_functions_example_validates_stored_checksums() -> None: assert set(out["corrupted_record"]) == {(5, "dave", 1234567890123456789, -8213464378072455284)} -def test_crc32_checksum_example_reports_current_main_head_behavior() -> None: +def test_crc32_checksum_example_partitions_frames_by_stored_checksum() -> None: + """`crc32_ethernet()` agrees with the stored CRCs from wirelog 0.54.0 on. + + Through wirelog 0.53.0 the built-in computed a checksum that matched no + stored value, so every frame landed in `corrupt_frame` and this test + pinned that as "current main head behavior". The expectations below are + wirelog's own golden output for `examples/05-crc32-checksum`, which + became a checked example in 0.54.0. + """ + if _wirelog_ver() < (0, 54, 0): + pytest.skip("crc32_ethernet() matches its stored checksums from wirelog 0.54.0 on") + mod = _import_example("05_crc32_checksum") out = mod.run() - assert out["valid_frame"] == [] + assert set(out["valid_frame"]) == { + ("F001", "DEADBEEF0102030405060708", 3838819244), + ("F002", "CAFEBABE0A0B0C0D0E0F1011", 250819451), + ("F003", "AABBCCDD1213141516171819", 1661565857), + ("F005", "FFEEDDCCBBAA998877665544", 2259087609), + } assert set(out["corrupt_frame"]) == { - ("F001", 3838819244, 2844319735), - ("F002", 250819451, 3954038922), - ("F003", 1661565857, 767742221), - ("F004", 9999999999, 1877464688), - ("F005", 2259087609, 2054014018), - ("F006", 1234567890, 944292671), + ("F004", 9999999999, 2056678491), + ("F006", 1234567890, 1992588221), } diff --git a/tests/test_batch.py b/tests/test_batch.py index 251b4c8..3da0724 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -9,6 +9,8 @@ import pytest from pyrewire._core.errors import ExecError +from pyrewire._ffi._loader import _parse_version, _pep440_base +from pyrewire._ffi._util import wirelog_version from pyrewire.batch import BatchProgram, Result # ---------------------------------------------------------------------- @@ -61,6 +63,62 @@ def test_evaluate_without_optimize_is_allowed(): res.close() +# A rule with four or more body atoms lets SIP insert a semijoin with +# something resolved above it. Before wirelog#955 the semijoin widened the +# reported output layout by the right relation's arity, shifting every +# column resolved above it; the out-of-range lookup then fell back to +# column 0, so the last head variable silently came back as 0 (#180). +# +# The corruption is silent — `evaluate()` succeeds and returns the right +# row count and arity — so this compares the optimized result against the +# unoptimized one instead of just asserting evaluation worked. +_SEMIJOIN_LAYOUT_SRC = """ +.decl typ(s: int32, c: int32) +.decl mand(c: int32) +.decl ord(s: int32, o: int32) +.decl rsn(s: int32, r: int32) +.decl out(o: int32, s: int32, r: int32) + +out(O, S, R) :- mand(C), typ(S, C), ord(S, O), rsn(S, R). + +typ(1, 5). +mand(5). +ord(1, 10). +rsn(1, 7). +""" + + +def _semijoin_layout_rows(optimize: bool) -> list[tuple[int, ...]]: + with BatchProgram.from_string(_SEMIJOIN_LAYOUT_SRC) as bp: + if optimize: + bp.optimize() + bp.load_all_facts() + res = bp.evaluate() + try: + return sorted({tuple(int(v) for v in row) for row in res.relation("out")}) + finally: + res.close() + + +def _wirelog_older_than(minimum: tuple[int, int, int]) -> bool: + return _parse_version(_pep440_base(wirelog_version())) < minimum + + +@pytest.mark.skipif( + _wirelog_older_than((0, 54, 0)), + reason=( + "wirelog#955 (the #180 fix) first ships in wirelog 0.54.0; the " + "loader floor still admits 0.52.0, where this corrupts silently." + ), +) +def test_optimize_preserves_head_bindings_with_four_body_atoms(): + """`optimize()` must not change the answer (#180, wirelog#955).""" + optimized = _semijoin_layout_rows(optimize=True) + + assert optimized == _semijoin_layout_rows(optimize=False) + assert optimized == [(10, 1, 7)] + + # ---------------------------------------------------------------------- # Result (#18) # ---------------------------------------------------------------------- diff --git a/tests/test_changelog_format.py b/tests/test_changelog_format.py index 965dfcc..b604d5b 100644 --- a/tests/test_changelog_format.py +++ b/tests/test_changelog_format.py @@ -168,11 +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.4...HEAD" + "[Unreleased]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.5...HEAD" in changelog ) assert ( - "[1.0.4]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.3...v1.0.4" + "[1.0.5]: https://github.com/semantic-reasoning/PyreWire/compare/v1.0.4...v1.0.5" in changelog ) assert ( diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py index d7268bf..0fd25de 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 "668f82ad69c2bbfc8e8111839302adf1360f55da" in default + assert "9f80877c82564cb92ea45bd6fffc2d681b0e13de" in default def test_ci_matrix_drops_python_310(): diff --git a/tests/test_easy_session.py b/tests/test_easy_session.py index bddafe9..8fbafbd 100644 --- a/tests/test_easy_session.py +++ b/tests/test_easy_session.py @@ -8,6 +8,9 @@ import pytest from pyrewire import EasySession, ParseError +from pyrewire._core.errors import WirelogError +from pyrewire._ffi._loader import _parse_version, _pep440_base +from pyrewire._ffi._util import wirelog_version _FRIENDSHIP = """ .decl friend(a: symbol, b: symbol) @@ -18,6 +21,10 @@ _EDGE_INT = ".decl edge(x: int32, y: int32)\n" +def _wirelog_older_than(minimum: tuple[int, int, int]) -> bool: + return _parse_version(_pep440_base(wirelog_version())) < minimum + + def test_open_and_close_via_context_manager(): with EasySession(_EDGE_INT) as s: assert s is not None @@ -69,13 +76,24 @@ def test_remove_after_insert(): s.remove("friend", ["alice", "bob"]) -def test_insert_arity_mismatch_does_not_immediately_error(): - """wirelog_easy_insert does not validate arity at insert time; the - error surfaces later (at step/snapshot). Just verify the call - completes without crashing — the strict validation is the - responsibility of higher-level helpers.""" +@pytest.mark.skipif( + _wirelog_older_than((0, 54, 0)), + reason=( + "wirelog#1038 checks the insert width against the `.decl` on the " + "first insert from 0.54.0 on; older builds silently dropped the " + "surplus value, and the loader floor still admits 0.52.0." + ), +) +def test_insert_arity_mismatch_raises(): + """A row wider than the `.decl` is rejected (wirelog#1038). + + Before wirelog 0.54.0 the relation's width was set lazily from + whatever the first producer supplied and nothing compared it against + the declaration, so this insert silently dropped the third value. + """ with EasySession(_EDGE_INT) as s: - s.insert("edge", [1, 2, 3]) # too many values; accepted at this layer + with pytest.raises(WirelogError): + s.insert("edge", [1, 2, 3]) # `edge` declares two columns def test_unsupported_row_value_type_raises(): diff --git a/tests/test_release_metadata.py b/tests/test_release_metadata.py index 571e1df..467141a 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_104(): - assert _pyproject()["project"]["version"] == "1.0.4" - assert _runtime_version_literal() == "1.0.4" +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_classifiers_mark_stable_python_311_through_314():