Skip to content

example.sudoku: assert in examine_potentials aborts the example in non-NDEBUG builds #2105

@yunyi1201

Description

@yunyi1201

The example.sudoku example aborts on its very first solve in any build that does not define NDEBUG (e.g. -DCMAKE_BUILD_TYPE=Debug, sanitizer builds, distro packages built without -DNDEBUG):

$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DSTDEXEC_BUILD_EXAMPLES=ON
$ cmake --build build --target example.sudoku
$ ./build/examples/example.sudoku
Assertion failed: (!"potential set is not a power of 2"),
function examine_potentials, file sudoku.cpp, line 282.
Abort trap: 6     # exit 134

Reproduces on current main (fee4d651), macOS arm64, Apple Clang, -std=gnu++20. The flags emitted by CMake for the Debug configuration are simply -g -std=gnu++20 ..., with no -DNDEBUG, so assert is active.

Diagnosis

Adding a print before the failing assert shows the very first call into examine_potentials already hits the default branch:

DBG: cell i=0 solved_element=1 potential_set=0x0

i.e. cell 0 of init_values0 (which starts as a fixed 1) reaches examine_potentials with potential_set == 0, falls through the early "empty set" check, and lands in the switch's default.

Root cause

calculate_potentials unconditionally clears potential_set for every cell and only fills in candidate bits for unsolved cells:

for (unsigned i = 0; i < BOARD_SIZE; ++i) {
  b[i].potential_set = 0;
  if (!b[i].solved_element) {
    // ... compute candidate bitmask ...
    b[i].potential_set |= 1 << (potential - 1);
  }
}

Every already-solved cell therefore enters examine_potentials with potential_set == 0. The early-out at the top of examine_potentials only treats potential_set == 0 as "empty set / dead branch" when the cell is also unsolved:

if (b[i].solved_element == 0 && b[i].potential_set == 0)  // empty set
  return false;
switch (b[i].potential_set) {
  case 1:  /* singleton -> promote */ break;
  ...
  case 256: ...
  default:
    assert(!"potential set is not a power of 2");   // sudoku.cpp:282
}

For an already-solved cell (solved_element != 0, potential_set == 0) control falls through into switch(0) -> default -> assert. There is also a second legal state the switch does not cover: an unsolved cell with multiple candidates, where potential_set is e.g. 3 = 1|2 or 7 = 1|2|4 and is not a power of two.

In other words, the assertion encodes an invariant ("potential_set is always a power of two here") that the function itself never establishes. Its actual intent is to promote singleton candidate sets into solved_element and leave every other cell untouched.

Suggested fix

Make the singleton filter explicit, instead of relying on the default assert being compiled out:

bool examine_potentials(board_element *b, bool *progress)
{
  bool singletons = false;
  for (unsigned i = 0; i < BOARD_SIZE; ++i)
  {
    if (b[i].solved_element != 0)
      continue;                      // already fixed; potential_set is 0 by construction
    if (b[i].potential_set == 0)     // empty set -> dead branch
      return false;
    // multiple candidates (not a power of 2): leave for partial_solve to branch on
    if ((b[i].potential_set & (b[i].potential_set - 1)) != 0)
      continue;
    switch (b[i].potential_set) {
      /* unchanged singleton -> solved_element mapping */
    }
  }
  *progress = singletons;
  return valid_board(b);
}

x & (x - 1) == 0 is the standard power-of-two test (x == 0 already excluded above). With the filter in place the default branch becomes genuinely unreachable; I'd remove it, but happy to keep it as a defensive assert if maintainers prefer.

After this change, example.sudoku runs to completion in both Debug and Release on all four built-in boards, in both find-one and full-enumeration modes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions