Skip to content

utils, tests: +feat Assert the universal iterator precondition and add randomized oracle tests - #953

Merged
Adda0 merged 1 commit into
develfrom
issue-797-utils-oracles-pr
Oct 16, 2026
Merged

Adda0 merged 1 commit into
develfrom
issue-797-utils-oracles-pr

Conversation

@Adda0

@Adda0 Adda0 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

The two remaining actionable items of #797: item 6 (precondition of the universal iterator) and item 7 (randomized oracle tests for the utility headers). The #769 container fixes these oracles depend on landed in #946. Item 3 (packing a 32-bit State pair in PairHash) stays blocked on #789 / #939 and is not part of this pull request.

Item 6: precondition of SynchronizedUniversalIterator::advance()

Universal synchronization over zero inputs is not meaningful. advance() now documents "at least one input pushed since construction or the last reset()" as its precondition and asserts it in debug builds (MATA_ASSERT(!this->positions.empty())). Release builds keep returning false. The library callers (NFT intersection and composition) always push two inputs. The header now also includes the <cstddef>, <vector> and assert.hh it uses.

The existing unit tests that called advance() on an iterator without inputs now check only the state after reset(). The release fallback has its own #ifdef NDEBUG test case.

Item 7: seeded randomized oracle tests

Every value is drawn from raw std::mt19937 output over a fixed seed range, as in tests/nfa/nfa-differential.cc, so the tests are deterministic across standard libraries:

Test case Oracle Seeds
OrdVector against std::set (randomized): construction (vector, std::set, iterator range), set_union and insert(OrdVector), intersection, difference, is_subset_of, is_intersection_empty_with, are_disjoint, ==/<, contains/count/find, min/max, random insert/erase sequences, erase_if std::set and std::ranges set algorithms 500
filter() and filter_indexes() against std::erase_if (randomized): the free functions and the OrdVector wrappers std::erase_if, an index mask 500
SparseSet against std::set (randomized): 60-step traces of insert/erase/complement/filter/rename (a permutation of [0, 61))/truncate/sort, then size, empty, max, membership over the whole domain, are_disjoint, intersects_with, copies std::set. Contents are compared as sorted element lists: SparseSet::operator== compares the internal buffers, which is a separate #769 item left untouched here. 300
SynchronizedUniversalIterator against a brute-force intersection (randomized): 2–5 inputs, also after reset(), with the collected count required to stay within the oracle's size so a stalling iterator fails instead of timing out std::ranges::set_intersection 300
SynchronizedExistentialIterator against a brute-force merge (randomized): 1–5 inputs, per-value count of synchronized positions, also after reset(), with the collected count likewise bounded by the oracle's size std::ranges::set_union, binary_search 300

Verification

  • nix fmt on the touched files: (0 changed).
  • just cpp::build debug + ./build/debug/g++/tests/tests on the pushed head, rebased onto devel 8b4c09b (GCC, Nix dev shell): test cases: 374 | 368 passed | 2 skipped | 4 failed as expected, assertions: 4754234 | 4754197 passed | 37 failed as expected. Release build: test cases: 375 | 369 passed | 2 skipped | 4 failed as expected, assertions: 4754239 | 4754202 passed | 37 failed as expected (the extra case is the release-only zero-input test). The 4 expected failures are the existing [!shouldfail] cases in tests/nft/nft-differential.cc.
  • The assertion fires in debug. A probe that calls advance() on an empty SynchronizedUniversalIterator, compiled without NDEBUG, prints include/mata/utils/synchronized-iterator.hh:117: advance: Assertion '!this->positions.empty()' failed. and exits with 134 (SIGABRT). Compiled with -DNDEBUG, it returns false.
  • The oracles catch real bugs. I compiled the three test files against a copy of the headers with the following re-introduced: the devel filter/filter_indexes (reserve(last)), the devel universal restart check, a SparseSet::complement that keeps values above the new domain, an OrdVector::difference that drops the last element, and an existential iterator that keeps at most two synchronized positions. All five randomized test cases failed (10 of 21 test cases, 2237 assertions); with the real headers all pass.
  • Performance: these are test and debug-only changes, so release code is unchanged and nothing needs measuring.

Part of #797.

@Adda0
Adda0 force-pushed the issue-769-container-traps-pr branch from e1d2d42 to 863e2ca Compare October 16, 2026 05:46
Base automatically changed from issue-769-container-traps-pr to devel October 16, 2026 06:14
@Adda0
Adda0 force-pushed the issue-797-utils-oracles-pr branch from 8b898e2 to b88e1a7 Compare October 16, 2026 07:05
@Adda0

Adda0 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto devel (7efcb1e) now that #946 has landed. The base was retargeted to devel and the pull request was DIRTY. Against the squash-merged #946 the rebase is a no-op (jj interdiff against the old head 8b898e2 is empty). The later devel commits (#887, #954, #957, #863, #889, #875, #949) do not touch the six files of this pull request. There are no review threads, so there were no code changes. just tests-cpp::check on the pushed head b88e1a7: Debug 364 | 358 passed | 2 skipped | 4 failed as expected, Release 365 | 359 passed | 2 skipped | 4 failed as expected. nix fmt on the touched files: 0 changed. I removed the "Stacked on #946" note from the description and updated the test counts.

@Adda0

Adda0 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Low impact] The runtime effect stays within one utility iterator's debug-time caller contract.

The PR appears safe to merge, with two non-blocking test improvements.

Findings

  1. P2 Broken iteration can stall tests ▶
  2. P2 Inputs do not share storage ▶

Summary

This PR adds a debug assertion for zero-input universal iteration and seeded oracle tests for the utility containers and iterators.

  • Universal iteration now requires at least one input.
  • Randomized checks compare OrdVector set operations with std::set.
  • Randomized checks compare value and index filters with simple references.
  • Randomized checks compare SparseSet updates and queries with std::set.
  • Randomized checks compare synchronized iterator results with set operations.

Reviews (1) · Last reviewed commit: "utils, tests: +feat Assert the universal..." · Reviewed by Greptile

Comment on lines +272 to +277
while (sync_iterator.advance()) {
const auto& current{sync_iterator.get_current()};
REQUIRE(current.size() == num_inputs);
const int value{*current[0]};
for (const auto& position : current) { REQUIRE(*position == value); }
synchronizations.push_back(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Broken iteration can stall tests

The new collection loops have no limit on returned values. If advance() regresses and repeatedly returns the same result, these tests keep growing a vector until the job times out or runs out of memory. Add a REQUIRE inside each loop that limits the collected count to the oracle's size, including the runs after reset().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: every collection loop now has REQUIRE(collected.size() <= oracle.size()) right after the push, in the first run and in the run after reset(), for both the universal (intersection) and the existential (merged) iterator. A stalling advance() now fails the REQUIRE instead of growing the vector until timeout.

Comment thread tests/synchronized-iterator.cc Outdated
INFO("seed " << seed);
const size_t num_inputs{2 + draw(rng, 4)};
const size_t universe{6 + draw(rng, 20)};
// All inputs share one buffer, so the outcome cannot depend on where they are allocated.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inputs do not share storage

The comment says all inputs share one buffer, but each inner std::vector<int> owns a separate buffer. This matters because the earlier iterator bug depended on where separate buffers were allocated. Remove the shared-buffer claim, or store the inputs as ranges within one buffer as the existing regression test does.

Suggested change
// All inputs share one buffer, so the outcome cannot depend on where they are allocated.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed as suggested: the claim was wrong (each inner vector owns its buffer), so the comment is removed. The buffer placement is irrelevant to the oracle check either way: the test compares only values against std::ranges::set_intersection.

…d randomized oracle tests

The two remaining actionable items of #797: item 6 (precondition of the universal iterator) and item 7 (randomized oracle tests for the utility headers). The #769 container fixes these oracles depend on landed in #946. Item 3 (packing a 32-bit `State` pair in `PairHash`) stays blocked on #789 / #939 and is not part of this pull request.

## Item 6: precondition of `SynchronizedUniversalIterator::advance()`

Universal synchronization over zero inputs is not meaningful. `advance()` now documents "at least one input pushed since construction or the last `reset()`" as its precondition and asserts it in debug builds (`MATA_ASSERT(!this->positions.empty())`). Release builds keep returning `false`. The library callers (NFT intersection and composition) always push two inputs. The header now also includes the `<cstddef>`, `<vector>` and `assert.hh` it uses.

The existing unit tests that called `advance()` on an iterator without inputs now check only the state after `reset()`. The release fallback has its own `#ifdef NDEBUG` test case.

## Item 7: seeded randomized oracle tests

Every value is drawn from raw `std::mt19937` output over a fixed seed range, as in `tests/nfa/nfa-differential.cc`, so the tests are deterministic across standard libraries:

| Test case | Oracle | Seeds |
|---|---|---|
| `OrdVector against std::set (randomized)`: construction (vector, `std::set`, iterator range), `set_union` and `insert(OrdVector)`, `intersection`, `difference`, `is_subset_of`, `is_intersection_empty_with`, `are_disjoint`, `==`/`<`, `contains`/`count`/`find`, `min`/`max`, random insert/erase sequences, `erase_if` | `std::set` and `std::ranges` set algorithms | 500 |
| `filter() and filter_indexes() against std::erase_if (randomized)`: the free functions and the `OrdVector` wrappers | `std::erase_if`, an index mask | 500 |
| `SparseSet against std::set (randomized)`: 60-step traces of `insert`/`erase`/`complement`/`filter`/`rename` (a permutation of `[0, 61)`)/`truncate`/`sort`, then `size`, `empty`, `max`, membership over the whole domain, `are_disjoint`, `intersects_with`, copies | `std::set`. Contents are compared as sorted element lists: `SparseSet::operator==` compares the internal buffers, which is a separate #769 item left untouched here. | 300 |
| `SynchronizedUniversalIterator against a brute-force intersection (randomized)`: 2–5 inputs, also after `reset()`, with the collected count required to stay within the oracle's size so a stalling iterator fails instead of timing out | `std::ranges::set_intersection` | 300 |
| `SynchronizedExistentialIterator against a brute-force merge (randomized)`: 1–5 inputs, per-value count of synchronized positions, also after `reset()`, with the collected count likewise bounded by the oracle's size | `std::ranges::set_union`, `binary_search` | 300 |

## Verification

* `nix fmt` on the touched files: `(0 changed)`.
* `just cpp::build debug` + `./build/debug/g++/tests/tests` on the pushed head, rebased onto `devel` `8b4c09b` (GCC, Nix dev shell): `test cases: 374 | 368 passed | 2 skipped | 4 failed as expected`, `assertions: 4754234 | 4754197 passed | 37 failed as expected`. Release build: `test cases: 375 | 369 passed | 2 skipped | 4 failed as expected`, `assertions: 4754239 | 4754202 passed | 37 failed as expected` (the extra case is the release-only zero-input test). The 4 expected failures are the existing `[!shouldfail]` cases in `tests/nft/nft-differential.cc`.
* **The assertion fires in debug.** A probe that calls `advance()` on an empty `SynchronizedUniversalIterator`, compiled without `NDEBUG`, prints `include/mata/utils/synchronized-iterator.hh:117: advance: Assertion '!this->positions.empty()' failed.` and exits with 134 (SIGABRT). Compiled with `-DNDEBUG`, it returns `false`.
* **The oracles catch real bugs.** I compiled the three test files against a copy of the headers with the following re-introduced: the `devel` `filter`/`filter_indexes` (`reserve(last)`), the `devel` universal restart check, a `SparseSet::complement` that keeps values above the new domain, an `OrdVector::difference` that drops the last element, and an existential iterator that keeps at most two synchronized positions. All five randomized test cases failed (10 of 21 test cases, 2237 assertions); with the real headers all pass.
* Performance: these are test and debug-only changes, so release code is unchanged and nothing needs measuring.

Part of #797.
@Adda0
Adda0 force-pushed the issue-797-utils-oracles-pr branch from b88e1a7 to 37eb8f3 Compare October 16, 2026 10:58
@Adda0

Adda0 commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto the latest devel (8b4c09b; the branch was DIRTY after #947 merged, the predicted append-append conflict in tests/ord-vector.cc) and addressed both review threads: the oracle loops now REQUIRE that the collected count never exceeds the oracle's size (including after reset()), and the incorrect shared-buffer comment is removed. just cpp::build + full test suite on the pushed head: Debug 374 | 368 passed | 2 skipped | 4 failed as expected, Release 375 | 369 passed | 2 skipped | 4 failed as expected; nix fmt on the touched files (0 changed). The oracle rows in the description document the new size bounds.

@Adda0
Adda0 merged commit 2dbaa03 into devel Oct 16, 2026
24 checks passed
@Adda0
Adda0 deleted the issue-797-utils-oracles-pr branch October 16, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant