Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Enumerator

Enumeration is genre.enumerate — an fn pointer on the runtime &'static Genre handle that streams uniquely-solvable puzzles for a given size, calling on_puzzle for each hit and on_stats periodically for progress UIs.

pub enumerate: fn(
    &EnumConfig,
    &AtomicBool,
    &mut dyn FnMut(&Puzzle),
    &mut dyn FnMut(EnumStats),
) -> String,

The default behind genre.enumerate is walk_canonical — a true canonical-form walker shared across every genre, driven entirely by the runtime Genre handle (genre.material_vocab, genre.derive_clue_at, genre.clue_domain, genre.empty_at, genre.build_constraints). No genre overrides it today.

Clue derivation per cell goes through genre.derive_clue_at(p, sol, coord) -> Option<ClueValue>. The default body dispatches through genre.clue_pattern (the canonical Pin / Count / ConnectedSize shortcut for u8 genres); future multi-byte clue genres (Tapa, Yajilin) override derive_clue_at directly. See the ClueValue discussion in Traits → Puzzle for the full picture.

The walker has three nested phases:

  1. Layouts. Walk the cell-layer material space at the given size, in canonical-form order under D4 (square) / D2 (non-square) lex-min. Genres with empty material_vocab (sudoku, slitherlink) collapse to a single all-floor layout.
  2. Completions. Bare-solve each layout to enumerate the legal full assignments under the genre’s interior rules (no clues active). Skip completions whose maximally-clued puzzle isn’t uniquely solvable — no clue subset of an ambiguous full-clue set can be unique.
  3. Clue subsets. For each accepted completion, walk lex-ascending k-combinations of clueable cells for k = 1, 2, … Smallest-puzzle-first ordering. Stabilizer-based dedup filters out clue arrangements that are D4-images of each other within a single layout’s orbit, before invoking the solver.

A monotonicity short-circuit runs alongside the subset walk: if a candidate subset M contains some previously-confirmed unique subset R as a strict superset, M must also be unique (extra clues are consistent with the same bare-solve completion, so adding them can’t introduce a second solution). The walker tracks recorded unique masks in popcount-bucketed Vec<u64> per completion and skips the solver call when M is subsumed. Solving was ~90% of per-subset cost, so this typically yields an order-of-magnitude speedup once minimal-unique anchors fill in at low k.

See notes/canonical-enumeration.md for the full design rationale, soundness arguments for the completions skip, and follow-on directions (value-relabeling-aware dedup, beautification).

EnumConfig

pub struct EnumConfig {
    pub dims: Dimensions,
    pub limit: usize,             // 0 = unlimited
    pub time_budget: Duration,
    pub holes: bool,              // include Material::Hole in the cell alphabet
    pub max_bare_solutions: usize, // cap on completions per layout (default 10_000)
}

pub struct EnumStats {
    pub layouts_examined: u64,
    pub total_layouts: u64,
    pub completions_examined: u64,
    pub subsets_examined: u64,
    pub unique: u64,
}

holes widens akari from 2-state (floor/wall) to 3-state (floor/wall/hole). Other genres don’t declare Hole on the cell layer, so the flag is a no-op for them. Default is true; set to false for larger akari grids where the 3-state space blows up.

max_bare_solutions caps the bare-solve completion enumeration per layout. Sudoku 4×4 has exactly 288 completions; slither 4×4 has thousands. The cap mostly exists so a pathological scaffold with millions of legal completions can’t hang the walker.

The return type is String — a free-form “stop reason” the caller echoes back ("exhausted", "limit", "deadline", "cancelled", or "bad-config:<why>").

Why callbacks, not iterators

Every real caller wants two things an iterator handles awkwardly:

  • Progress reporting, since canonical-form scans can examine millions of layouts even at small sizes; silent UIs are broken.
  • Early exit on multiple conditions — output limit, wall-clock budget, user cancel. Returning an iterator forces every consumer to re-thread those exit conditions through the pull loop; baking them into the contract keeps callers trivial.

cancel is a separate &AtomicBool rather than a config field so EnumConfig stays Serialize + DeserializeOwned — runtime cancellation has no business in a serialized config.

Discovery and coverage (planned)

The discovery + coverage feedback loop is described elsewhere — pair the enumerator with the technique library and the trial-search oracle to mine specimens (small puzzles annotated with what was hard about them), then run every technique against every persisted specimen for per-technique hit counts and coverage residue.

The CLI verbs discover and coverage are placeholders for that loop; the mining loop runs ad-hoc against Genre::enumerate until those land.