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.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
Expand Down
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
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.53.0, using peeled SHA
`668f82ad69c2bbfc8e8111839302adf1360f55da`.
The bundled library is built from wirelog v0.54.0, using peeled SHA
`9f80877c82564cb92ea45bd6fffc2d681b0e13de`.

## 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 @@ -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).
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.4"
version = "1.0.5"
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 = "668f82ad69c2bbfc8e8111839302adf1360f55da" }
environment = { WIRELOG_VERSION = "9f80877c82564cb92ea45bd6fffc2d681b0e13de" }

[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 = "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
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 = "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}"

[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}"
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.4"
__version__ = "1.0.5"
__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 = "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"]
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.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
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,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:
Expand Down Expand Up @@ -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")

Expand Down
28 changes: 20 additions & 8 deletions tests/integration/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}


Expand Down
58 changes: 58 additions & 0 deletions tests/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ----------------------------------------------------------------------
Expand Down Expand Up @@ -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)
# ----------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions tests/test_changelog_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ci_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
30 changes: 24 additions & 6 deletions tests/test_easy_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down
6 changes: 3 additions & 3 deletions tests/test_release_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading