From 9632634b648109765955e89ec07f788388d8170f Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Sat, 29 Aug 2026 19:26:49 -0700 Subject: Fix batch-mode exit code, killer-table backfill collision, impossible QSearch mate-magnitude asserts, MATEMOVE PV display, --command truncation, and test.sh's stale egtbpath. Several small, independent correctness fixes bundled together since they were all exercised together through today's precommit_check.sh and curated-suite runs: - command.c: batch-mode's "Exhausted input" exit was exit(-1), which truncates to 255 (an 8-bit status) and is indistinguishable from a real crash's nonzero exit. Changed to exit(0) so debug_smoke_test.sh can reliably tell a clean batch run apart from a crash by exit status alone. - dynamic.c: _NewKillerMove's slot[1] backfill from mvNullmoveQuietRefutations[uPly] had no check that the backfilled move differed from the move just placed in slot[0]. When they coincided, both slots held the identical move, silently wasting a killer slot in release builds (ASSERT is a no-op there) and tripping _NewKillerMove's own IS_SAME_MOVE invariant in DEBUG builds. Fixed by skipping the backfill on collision. - search.c: removed two ASSERT(iBestScore > -NMATE) calls in QSearch that encoded an invariant that isn't actually guaranteed -- at an early full-width root iteration, or after aspiration-window widening following repeated fail-highs, an ancestor frame's iAlpha/iBeta can itself already be more extreme than -NMATE with no mate anywhere in the line, so a legitimate fail-low placeholder or fail-high score can land in mate-magnitude territory purely as a window artifact. hash.c's storage path already treats any value <= -NMATE as a sound upper bound regardless of origin, so this was a false invariant, not a caught bug. Also: minor whitespace cleanup, an added ASSERT documenting the futility-margin depth precondition it replaced a redundant runtime check for, and PV/leaf-count bookkeeping on the mate/draw-at-root leaf paths that was previously skipped. - util.c: MATEMOVE sentinel moves weren't handled in PV-to-string conversion, so a PV ending in a detected mate would either display garbage or hit the same-move assert. Added an explicit "<#>" marker. - test.sh: --egtbpath pointed at a nonexistent /egtb/three;/egtb/four; /egtb/five; corrected to /zscratch/egtb, this box's actual EGTB location. - main.c/input.c: --command's initial-command buffer (g_szInitialCommand) was a fixed 256-byte array; strncpy(..., SMALL_STRING_LEN_CHAR - 2) silently truncated any longer --command string, and -- worse -- when the source was long enough not to fit, strncpy doesn't null-terminate the destination, so the immediately-following strcat(..., "\r\n") could read/write past the buffer. Long move-replay command strings used during this session's debugging hit the truncation directly (a ~600 char move list silently cut off mid-token, desyncing the input queue). Changed g_szInitialCommand to a heap allocation sized to the actual input length instead of a fixed cap. - CLAUDE.md: documents the above (this file's own diff is prior session's writeup of these same fixes, committed now alongside the code). All exercised together via precommit_check.sh (self-test suite + DEBUG smoke test against random ecm.ep_ samples) and the sd10/sn5m curated suite sweep run for the eval.c hand-tuning commit just before this one. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M9ZDiJhiUajUxh95mTXCFJ --- src/search.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) (limited to 'src/search.c') diff --git a/src/search.c b/src/search.c index 61fa428..ab7d122 100755 --- a/src/search.c +++ b/src/search.c @@ -358,7 +358,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, if (iScore > iBeta) { StoreLowerBound(mvHash, pos, iScore, uDepth, FALSE); } - iBestScore = iScore; // TODO: try just beta here + iBestScore = iScore; goto end; } } @@ -516,8 +516,8 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, (ctx->uPly >= 2) && (ctx->sPlyInfo[ctx->uPly - 2].iExtensionAmount <= 0)) { - if ((uDepth > THREE_QUARTERS_PLY) && - (uDepth <= ONE_PLY + THREE_QUARTERS_PLY) && + ASSERT(uDepth >= THREE_QUARTERS_PLY); + if ((uDepth <= ONE_PLY + THREE_QUARTERS_PLY) && (iRoughEval + VALUE_KNIGHT <= iAlpha)) { uFutilityMargin = (iAlpha - iRoughEval) / 2; @@ -762,9 +762,9 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, (uFutilityMargin) && (TRUE == fIsLeftoverMove) && (iExtend <= 0) && - (!IS_ESCAPING_CHECK(mv)) && + (!IS_ESCAPING_CHECK(mv)) && (!IS_CAPTURE_OR_PROMOTION(mv)) && - (!IS_CHECKING_MOVE(mv)) && + (!IS_CHECKING_MOVE(mv)) && (!fThreat) && (ComputeMoveScore(ctx, mv, (x - 1)) < uFutilityMargin)) { @@ -843,7 +843,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, iBestScore = iScore; mvBest = mv; pi->mvBest = mv; - + if (iScore > iAlpha) { if (iScore >= iBeta) @@ -919,6 +919,11 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, ASSERT(IS_CHECKING_MOVE(mvLast)); ASSERT(InCheck(pos, pos->uToMove)); iBestScore = MATED_SCORE(ctx->uPly); + if ((iAlpha < iBestScore) && (iBestScore < iBeta)) + { + INC(ctx->sCounters.tree.u64LeafCount); + UpdatePV(ctx, MATEMOVE); + } ASSERT(iBestScore <= -NMATE); goto end; } @@ -927,6 +932,7 @@ Search(IN SEARCHER_THREAD_CONTEXT *ctx, iBestScore = 0; if ((iAlpha < iBestScore) && (iBestScore < iBeta)) { + INC(ctx->sCounters.tree.u64LeafCount); UpdatePV(ctx, DRAWMOVE); } goto end; @@ -1460,7 +1466,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, // If we see a move whose value is zero, the rest of the moves // in this ply can be tossed. ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE | VERIFY_AFTER)); - ASSERT(iBestScore > -NMATE); goto end; } mv = ctx->sMoveStack.mvf[x].mv; @@ -1522,7 +1527,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, mv.cTo); } KEEP_TRACK_OF_FIRST_MOVE_FHs(uLegalMoves == 1); - ASSERT(iBestScore > -NMATE); ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE)); goto end; } @@ -1546,7 +1550,6 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, } } } - ASSERT(iBestScore > -NMATE); ASSERT(SanityCheckMoves(ctx, x, VERIFY_BEFORE)); end: @@ -1554,5 +1557,10 @@ QSearch(IN SEARCHER_THREAD_CONTEXT *ctx, ASSERT(PositionsAreEquivalent(pos, &pi->sPosition)); ASSERT(IS_VALID_SCORE(iBestScore) || WE_SHOULD_STOP_SEARCHING); DTLeaveNode(ctx, TRUE, iBestScore, pi->mvBest); + + // Note: iBestScore can be +INFINITY or -INFINITY here even in the + // absence of a legitimate mate detected if we disallowed stand + // pat due to perceived danger early on, when the a..b window had + // an extreme bound. This is "legitimate" but weird. return(iBestScore); } -- cgit v1.3