From 94d5eee2656e988aa109d86f58012acfa69271d9 Mon Sep 17 00:00:00 2001 From: Dubslow Date: Sat, 27 Jun 2026 06:40:20 -0500 Subject: [PATCH 1/2] Some search comment tweaks (grammar, redundancy, clarification) no functional change --- src/search.cpp | 92 ++++++++++++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index e8651b08793..cbdd9b4c018 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -769,7 +769,7 @@ Value Search::Worker::search( if (!rootNode) { - // Step 2. Check for aborted search and immediate draw + // Step 2. Check for aborted search or immediate draw if (threads.stop.load(std::memory_order_relaxed) || pos.is_draw(ss->ply) || ss->ply >= MAX_PLY) return (ss->ply >= MAX_PLY && !ss->inCheck) ? evaluate(pos) : value_draw(nodes); @@ -777,9 +777,8 @@ Value Search::Worker::search( // Step 3. Mate distance pruning. Even if we mate at the next move our score // would be at best mate_in(ss->ply + 1), but if alpha is already bigger because // a shorter mate was found upward in the tree then there is no need to search - // because we will never beat the current alpha. Same logic but with reversed - // signs apply also in the opposite condition of being mated instead of giving - // mate. In this case, return a fail-high score. + // because we will never beat the current alpha. Equal and opposite logic applies + // when being mated. In either case, return a fail-high score. alpha = std::max(mated_in(ss->ply), alpha); beta = std::min(mate_in(ss->ply + 1), beta); if (alpha >= beta) @@ -855,13 +854,13 @@ Value Search::Worker::search( if (priorReduction >= 2 && depth >= 2 && ss->staticEval + (ss - 1)->staticEval > 166) depth--; - // At non-PV nodes we check for an early TT cutoff + // Step 6. At non-PV nodes we check for an early TT cutoff if (!PvNode && !excludedMove && ttData.depth > depth - (ttData.value <= beta) && is_valid(ttData.value) // Can happen when !ttHit or when access race in probe() && (ttData.bound & (ttData.value >= beta ? BOUND_LOWER : BOUND_UPPER)) && (cutNode == (ttData.value >= beta) || depth > 4)) { - // If ttMove is quiet, update move sorting heuristics on TT hit + // If the ttMove is quiet, update move sorting heuristics on TT hit if (ttData.move && ttData.value >= beta) { // Bonus for a quiet ttMove that fails high @@ -895,15 +894,15 @@ Value Search::Worker::search( else return ttData.value; } - } // No cutoff, but why? Does the stored inexact value mismatch our aspiration window? + } // No cutoff, but why? Compare the aspiration window to the inexact bound else if (!PvNode && !excludedMove && ttData.depth > depth - (ttData.value <= beta) && is_valid(ttData.value) && ttData.bound != BOUND_EXACT && ttData.bound & (ttData.value >= beta ? BOUND_UPPER : BOUND_LOWER) && depth > 5) - { // If a window-bound mismatch is the only reason cutoff failed, penalize the now-useless tte + { // If such a mismatch is the only reason cutoff failed, the tte is now useless ttWriter.penalize(1); } - // Step 6. Tablebases probe + // Step 7. Tablebases probe if (!rootNode && !excludedMove && tbConfig.cardinality) { int piecesCount = pos.count(); @@ -970,13 +969,13 @@ Value Search::Worker::search( } - // Step 7. Razoring + // Step 8. Razoring // If eval is really low, skip search entirely and return the qsearch value. // For PvNodes, we must have a guard against mates being returned. if (!PvNode && eval < alpha - 483 - 318 * depth * depth) return qsearch(pos, ss, alpha, beta); - // Step 8. Futility pruning: child node + // Step 9. Futility pruning: child node // The depth condition is important for mate finding. if (!ss->ttPv && depth < 19 && eval >= beta && (!ttData.move || ttCapture) && !is_loss(beta) && !is_win(eval)) @@ -992,7 +991,7 @@ Value Search::Worker::search( return (661 * beta + 363 * eval) / 1024; } - // Step 9. Null move search with verification search + // Step 10. Null move search with verification search if (cutNode && ss->staticEval >= beta - 13 * depth - 47 * improving + 365 && !excludedMove && pos.non_pawn_material(us) && ss->ply >= nmpMinPly && beta >= -2000) { @@ -1029,13 +1028,13 @@ Value Search::Worker::search( improving |= ss->staticEval >= beta; - // Step 10. Internal iterative reductions + // Step 11. Internal iterative reductions // At sufficient depth, reduce depth for PV/Cut nodes without a TTMove. // (*Scaler) Making IIR more aggressive scales poorly. if (!ss->followPV && !allNode && depth >= 6 && !ttData.move) depth--; - // Step 11. ProbCut + // Step 12. ProbCut // If we have a good enough capture (or queen promotion) and a reduced search // returns a value much above beta, we can (almost) safely prune the previous move. probCutBeta = beta + 241 - 64 * improving; @@ -1085,7 +1084,7 @@ Value Search::Worker::search( moves_loop: // When in check, search starts here - // Step 12. A small Probcut idea + // Step 13. A small ProbCut idea probCutBeta = beta + 428; if ((ttData.bound & BOUND_LOWER) && ttData.depth >= depth - 4 && ttData.value >= probCutBeta && !is_decisive(beta) && is_valid(ttData.value) && !is_decisive(ttData.value)) @@ -1103,7 +1102,7 @@ Value Search::Worker::search( int moveCount = 0; - // Step 13. Loop through all pseudo-legal moves until no moves remain + // Step 14. Loop through all pseudo-legal moves until no moves remain // or a beta cutoff occurs. while ((move = mp.next_move()) != Move::none()) { @@ -1149,7 +1148,7 @@ Value Search::Worker::search( if (ss->ttPv) r += 929; - // Step 14. Pruning at shallow depths. + // Step 15. Pruning at shallow depths. // Depth conditions are important for mate finding. if (!rootNode && pos.non_pawn_material(us) && !is_loss(bestValue)) { @@ -1220,14 +1219,16 @@ Value Search::Worker::search( } } - // Step 15. Extensions - // Singular extension search. If all moves but one - // fail low on a search of (alpha-s, beta-s), and just one fails high on - // (alpha, beta), then that move is singular and should be extended. To - // verify this we do a reduced search on the position excluding the ttMove - // and if the result is lower than ttValue minus a margin, then we will - // extend the ttMove. Recursive singular search is avoided. - + // Step 16. Singular Extensions + // + // We check for "only moves": if one move fails high on (alpha, beta) but all + // others fail low on (alpha-s, beta-s), then that move is singular. Singular + // moves are extended to better estimate the result of the (putatively) + // forced line. If it's non-singular, we may do some pruning or reduction. + // + // Recursive excluded search is avoided. The `excludedMove` mechanism + // was historically a hack and remains a bit fragile. + // // (*Scaler) Generally, higher singularBeta (i.e closer to ttValue) // and lower extension margins scale well. if (!rootNode && move == ttData.move && !excludedMove && depth >= 6 + ss->ttPv @@ -1286,12 +1287,14 @@ Value Search::Worker::search( u64 nodeCount = rootNode ? u64(nodes) : 0; - // Step 16. Make the move + // Step 17. Make the move do_move(pos, move, st, givesCheck, ss); // Add extension to new depth newDepth += extension; + // Step 18. Compute and apply late moves reduction (LMR) (or possibly extension) + // Decrease reduction for PvNodes (*Scaler) if (ss->ttPv) r -= 3023 + PvNode * 1004 + (ttData.value > alpha) * 885 @@ -1333,7 +1336,7 @@ Value Search::Worker::search( if (allNode) r += r * 276 / (256 * depth + 268); - // Step 17. Late moves reduction / extension (LMR) + // Apply the computed LMR if (depth >= 2 && moveCount > 1) { // In general we want to cap the LMR depth search at newDepth, but when @@ -1366,7 +1369,7 @@ Value Search::Worker::search( } } - // Step 18. Full-depth search when LMR is skipped + // Step 19. Full-depth search when LMR is skipped else if (!PvNode || moveCount > 1) { // Increase reduction if ttMove is not present @@ -1378,7 +1381,7 @@ Value Search::Worker::search( newDepth - (r > 5234) - (r > 5487 && newDepth > 2), !cutNode); } - // For PV nodes only, do a full PV search on the first move or after a fail high, + // Step 20. For PV nodes only, do a full PV search on the first move or after a fail high, // otherwise let the parent node fail low with value <= alpha and try another move. if (PvNode && (moveCount == 1 || value > alpha)) { @@ -1395,15 +1398,15 @@ Value Search::Worker::search( value = -search(pos, ss + 1, -beta, -alpha, newDepth, false); } - // Step 19. Undo move + // Step 21. Undo move undo_move(pos, move); assert(value > -VALUE_INFINITE && value < VALUE_INFINITE); - // Step 20. Check for a new best move - // Finished searching the move. If a stop occurred, the return value of - // the search cannot be trusted, and we return immediately without updating - // best move, principal variation nor transposition table. + // Step 22. Check for a new best move + // If a stop occurred, the value of the search cannot be trusted, and we + // return immediately without updating the best move, principal variation + // or transposition table. if (threads.stop.load(std::memory_order_relaxed)) return VALUE_ZERO; @@ -1472,13 +1475,13 @@ Value Search::Worker::search( ++bestMoveChanges; } else - // All other moves but the PV, are set to the lowest value: this + // All other moves but the PV are set to the lowest value: this // is not a problem when sorting because the sort is stable and the - // move position in the list is preserved - just the PV is pushed up. + // move position in the list is preserved -- just the PV is pushed up. rm.score = -VALUE_INFINITE; } - // In case we have an alternative move equal in eval to the current bestmove, + // If we have an alternative move equal in value to the current bestmove, we (sometimes) // promote it to bestmove by pretending it just exceeds alpha (but not beta). int inc = (value == bestValue && ss->ply + 2 >= rootDepth && (int(nodes) & 14) == 0 && !is_win(std::abs(value) + 1)); @@ -1522,10 +1525,11 @@ Value Search::Worker::search( } } - // Step 21. Check for mate and stalemate + // Step 23. Check for mate and stalemate // All legal moves have been searched and if there are no legal moves, it // must be a mate or a stalemate. If we are in a singular extension search then // return a fail low score. + // Otherwise, update histories and stats. assert(moveCount || !ss->inCheck || excludedMove || !MoveList(pos).size()); @@ -1586,7 +1590,7 @@ Value Search::Worker::search( if (bestValue <= alpha) ss->ttPv = ss->ttPv || (ss - 1)->ttPv; - // Write gathered information in transposition table. Note that the + // Step 24. Write gathered information in transposition table. Note that the // static evaluation is saved as it was before correction history. if (!excludedMove && !(rootNode && pvIdx)) ttWriter.write(posKey, value_to_tt(bestValue, ss->ply), ss->ttPv, @@ -1607,8 +1611,8 @@ Value Search::Worker::search( update_correction_history(pos, ss, *this, 1061 * bonus / 1024); } - assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); - + // The search is now complete. + assert(-VALUE_INFINITE < bestValue && bestValue < VALUE_INFINITE); return bestValue; } @@ -1842,14 +1846,14 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta) if (!is_decisive(bestValue) && bestValue > beta) bestValue = (462 * bestValue + 562 * beta) / 1024; - // Save gathered info in transposition table. The static evaluation + // Step 10. Save gathered info in transposition table. The static evaluation // is saved as it was before adjustment by correction history. ttWriter.write(posKey, value_to_tt(bestValue, ss->ply), pvHit, bestValue >= beta ? BOUND_LOWER : BOUND_UPPER, DEPTH_QS, bestMove, unadjustedStaticEval, tt.generation()); - assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); - + // The search is now complete. + assert(-VALUE_INFINITE < bestValue && bestValue < VALUE_INFINITE); return bestValue; } From 054f6196b22840c842c5ba63849bea8b67b59417 Mon Sep 17 00:00:00 2001 From: Dubslow Date: Wed, 1 Jul 2026 15:08:39 -0500 Subject: [PATCH 2/2] Rename RootMove inexact flags to match search `Bound` terminology Namely, in search we use `enum Bound`, and every score is by defintion a bound, it just may be a one-sided bound or two-sided bound. So the RootMove flags track inexactness, not the kind of bounds. Rename them accordingly Also constexpr and an assert no functional change --- src/search.cpp | 53 ++++++++++++++++++++++++++------------------------ src/search.h | 14 ++++++------- src/thread.cpp | 4 ++-- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/search.cpp b/src/search.cpp index cbdd9b4c018..dbc9912c52f 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -449,7 +449,7 @@ bool Search::Worker::iterative_deepening() { // when pvIdx - 1 is a proven loss. // Moreover, we do not trust an exact loss score from an aborted search. if ((is_loss(rootMoves[pvIdx - 1].score) && rootMoves[pvIdx] < rootMoves[pvIdx - 1]) - || rootMoves[pvIdx].score_is_exact_loss()) + || rootMoves[pvIdx].is_exact_loss()) { // If previousScore is exact and worse than pvIdx - 1, we can safely use it. // If it is equal, we make sure it cannot overtake pvIdx - 1. @@ -461,11 +461,11 @@ bool Search::Worker::iterative_deepening() { rootMoves[pvIdx].previousScore; rootMoves[pvIdx].previousScore = -VALUE_INFINITE; rootMoves[pvIdx].pv = rootMoves[pvIdx].previousPV; - rootMoves[pvIdx].unset_bound_flags(); + rootMoves[pvIdx].unset_inexact(); } // Otherwise, if we can, we cap the score to the best possible, and mark - // the score as a bound (also a valid excuse for the incomplete PV.) + // the score as inexact (also a valid excuse for the incomplete PV.) else { if (is_loss(rootMoves[pvIdx - 1].score)) @@ -474,19 +474,19 @@ bool Search::Worker::iterative_deepening() { rootMoves[pvIdx - 1].score; rootMoves[pvIdx].previousScore = -VALUE_INFINITE; rootMoves[pvIdx].pv.resize(1); - rootMoves[pvIdx].scoreUpperbound = true; + rootMoves[pvIdx].inexactUpper = true; } else - rootMoves[pvIdx].scoreUpperbound = false; + rootMoves[pvIdx].inexactUpper = false; - rootMoves[pvIdx].scoreLowerbound = !rootMoves[pvIdx].scoreUpperbound; + rootMoves[pvIdx].inexactLower = !rootMoves[pvIdx].inexactUpper; } } - // Finally, we mark all loss scores from partially searched moves as a bound. + // Finally, we mark all loss scores from partially searched moves as inexact. for (usize i = pvIdx + 1; i < multiPV; ++i) - if (rootMoves[i].score_is_exact_loss()) - rootMoves[i].scoreLowerbound = true; + if (rootMoves[i].is_exact_loss()) + rootMoves[i].inexactLower = true; } // Sort the PV lines searched so far and update the GUI @@ -505,7 +505,7 @@ bool Search::Worker::iterative_deepening() { const bool forgottenMate = lastBestMoveScore != -VALUE_INFINITE && is_mate_or_mated(lastBestMoveScore) && (std::abs(rootMoves[0].score) < std::abs(lastBestMoveScore) - || rootMoves[0].score_is_bound()); + || rootMoves[0].is_inexact()); if (!threads.stop) { @@ -520,7 +520,7 @@ bool Search::Worker::iterative_deepening() { } } - const bool abortedLossSearch = threads.stop && !pvIdx && rootMoves[0].score_is_exact_loss(); + const bool abortedLossSearch = threads.stop && !pvIdx && rootMoves[0].is_exact_loss(); // An exact mated-in/TB-loss score from an aborted search cannot be trusted: the // loss could be delayed or refuted upon exploring the remaining root-moves. @@ -536,14 +536,14 @@ bool Search::Worker::iterative_deepening() { const auto& rm) { return rm == lastPV[0]; }); rootMoves[0].score = rootMoves[0].uciScore = lastBestMoveScore; rootMoves[0].pv = lastBestMovePV; - rootMoves[0].unset_bound_flags(); + rootMoves[0].unset_inexact(); if (mainThread) uciPvSent = false; } - // For an aborted d1 search we label the loss score as a lower bound. + // For an aborted d1 search we label the loss score as inexact. else if (abortedLossSearch) - rootMoves[0].scoreLowerbound = true; + rootMoves[0].inexactLower = true; } // Have we found a "mate in x" after a completed iteration? @@ -1448,17 +1448,17 @@ Value Search::Worker::search( { rm.score = rm.uciScore = value; rm.selDepth = selDepth; - rm.unset_bound_flags(); + rm.unset_inexact(); if (value >= beta) { - rm.scoreLowerbound = true; - rm.uciScore = beta; + rm.inexactLower = true; + rm.uciScore = beta; } else if (value <= alpha) { - rm.scoreUpperbound = true; - rm.uciScore = alpha; + rm.inexactUpper = true; + rm.uciScore = alpha; } rm.pv.resize(1); @@ -2261,9 +2261,9 @@ void SearchManager::output_pv(Search::Worker& worker, v = isTBScore ? rootMoves[i].tbScore : v; // Potentially correct and extend the PV, and in exceptional cases v. - // Previous PVs have already been extended. Bound flags indicate an unreliable PV. + // Previous PVs have already been extended. Inexact flags indicate an unreliable PV. if (is_decisive(v) && !is_mate_or_mated(v) && !usePreviousScore - && (!rootMoves[i].score_is_bound() || isTBScore)) + && (!rootMoves[i].is_inexact() || isTBScore)) syzygy_extend_pv(worker.options, worker.limits, pos, rootMoves[i], v); std::string pv; @@ -2275,9 +2275,12 @@ void SearchManager::output_pv(Search::Worker& worker, pv.pop_back(); auto wdl = worker.options["UCI_ShowWDL"] ? UCIEngine::wdl(v, pos) : ""; - auto bound = rootMoves[i].scoreLowerbound - ? "lowerbound" - : (rootMoves[i].scoreUpperbound ? "upperbound" : ""); + + // Scores can't be both exact and inexact + assert(!(rootMoves[i].inexactLower && rootMoves[i].inexactUpper)); + auto bound = rootMoves[i].inexactLower ? "lowerbound" + : rootMoves[i].inexactUpper ? "upperbound" + : ""; InfoFull info; @@ -2287,7 +2290,7 @@ void SearchManager::output_pv(Search::Worker& worker, info.score = {v, pos}; info.wdl = wdl; - // TB and previous scores are exact, even though their bound flags may say otherwise. + // TB and previous scores are exact, even though their flags may say otherwise. if (!(isTBScore || usePreviousScore)) info.bound = bound; diff --git a/src/search.h b/src/search.h index 96927e7b5e7..4afbcfa86ab 100644 --- a/src/search.h +++ b/src/search.h @@ -127,14 +127,14 @@ struct RootMove { explicit RootMove(Move m) { pv.push_back(m); } bool extract_ponder_from_tt(const TranspositionTable& tt, Position& pos); - bool score_is_bound() const { return scoreLowerbound || scoreUpperbound; } - bool score_is_exact_loss() const { - return score != -VALUE_INFINITE && is_loss(score) && !score_is_bound(); + constexpr bool is_inexact() const { return inexactLower || inexactUpper; } + constexpr bool is_exact_loss() const { + return score != -VALUE_INFINITE && is_loss(score) && !is_inexact(); } - void unset_bound_flags() { scoreLowerbound = scoreUpperbound = false; } + constexpr void unset_inexact() { inexactLower = inexactUpper = false; } bool operator==(const Move& m) const { return pv[0] == m; } // Sort in descending order - bool operator<(const RootMove& m) const { + constexpr bool operator<(const RootMove& m) const { return m.score != score ? m.score < score : m.previousScore < previousScore; } @@ -144,8 +144,8 @@ struct RootMove { Value averageScore = -VALUE_INFINITE; Value meanSquaredScore = -VALUE_INFINITE * VALUE_INFINITE; Value uciScore = -VALUE_INFINITE; - bool scoreLowerbound = false; - bool scoreUpperbound = false; + bool inexactLower = false; // By default root scores are exact, unless flagged as a + bool inexactUpper = false; // one-sided bound here. See also `enum Bound` in types.h bool previousScoreExact = false; int selDepth = 0; int tbRank = 0; diff --git a/src/thread.cpp b/src/thread.cpp index e34d321c326..9b339c84cd9 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -372,10 +372,10 @@ Thread* ThreadPool::get_best_thread() const { // Aborted (d1) searches may lead to inexact win (or loss) scores. const bool bestThreadDecisive = bestThreadMove.score != -VALUE_INFINITE && is_decisive(bestThreadMove.score) - && !bestThreadMove.score_is_bound(); + && !bestThreadMove.is_inexact(); const bool newThreadDecisive = newThreadMove.score != -VALUE_INFINITE && is_decisive(newThreadMove.score) - && !newThreadMove.score_is_bound(); + && !newThreadMove.is_inexact(); if (bestThreadDecisive) {