summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAargh Rai <aargh.rai+git@gmail.com>2026-08-27 17:41:07 +0530
committerAargh Rai <aargh.rai+git@gmail.com>2026-08-27 17:41:07 +0530
commit5a02743c0c9563f41a8c3821a7bbce4fc3b0b839 (patch)
treef36aa805b8c90a8d786e8561a9d84f57e6191c19
parentd87b4744bed8f0c43e11ea12508ef2941bdd7186 (diff)
bug fixes
-rw-r--r--PLAN.md42
-rw-r--r--include/engine/search.h9
-rw-r--r--include/engine/threads.h17
-rw-r--r--src/engine/search.c160
-rw-r--r--src/engine/thread.c51
-rw-r--r--src/engine/threads.c105
-rw-r--r--src/main.c9
-rw-r--r--tests/generated.c14
8 files changed, 364 insertions, 43 deletions
diff --git a/PLAN.md b/PLAN.md
index 8d5fcad..da39fd7 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -9,8 +9,7 @@ primitives (HELP/Split/Merge/Share), sequential search, evaluation, iterative
deepening, root splitting, and child worker loop are all implemented and tested.
This plan maps the DTS concepts (Sections 2.3–2.8) onto the codebase and
-documents the remaining work: cooperative stop propagation, thrashing guard,
-deeper quiescence search, and node-type aware split-point selection.
+documents the remaining work: end-to-end UCI integration (H1).
---
@@ -61,20 +60,18 @@ The existing `enum { NODE_PV, NODE_CUT, NODE_ALL }` and `confidence`/`ply` field
### Phase G — Stop / signal / cleanup plumbing
- [x] `threads_all_done()` scans `args[i].finished` atomics — implemented and tested.
-- [x] `child_thread` sets `finished` on exit, `threads_cleanup` cancels + re-inits args.
-- [ ] **G2.** `main.c` currently uses `pthread_cancel` (hard kill) on `EVENT_ENGINE_STOP`.
- Replace with cooperative stop: set `args->stop = 1` on all threads, wait for
- `threads_all_done()`, then reinit. This lets threads clean up their split
- points and TT state gracefully.
+- [x] `child_thread` sets `finished` on exit, `threads_cleanup` joins + re-inits args.
+- [x] **G2.** `main.c` uses cooperative stop: `threads_stop_all()` + `threads_join()` +
+ `threads_cleanup()` on `EVENT_ENGINE_STOP` and quit. Threads clean up gracefully.
### Phase H — End-to-end + polish
- [ ] **H1.** `./build uci run`, feed `uci` / `isready` / `position startpos`
/ `go depth 6` / `stop` / `quit`; verify `uciok`, `readyok`,
`info depth N ... pv`, `bestmove`.
-- [ ] **H2.** Add thrashing counter to `split_point_t`; guard HELP when
+- [x] **H2.** Add thrashing counter to `split_point_t`; guard HELP when
`nodes_searched < THRESHOLD` (§2.1). Re-test.
-- [ ] **H3.** Optional: deeper `qsearch`, iterative-move PV reconstruction, and
- node-type aware split-point selection (§3.2) as time allows.
+- [x] **H3.** Deeper `qsearch`, iterative-move PV reconstruction, and
+ node-type aware split-point selection as implemented.
---
@@ -119,20 +116,21 @@ These are concrete, ordered steps. Each should be a single commit/patch and
build (`just test`) green before the next.
### Phase G — Stop / signal / cleanup
-- [ ] **G2.** In `src/main.c`, replace `pthread_cancel` on `EVENT_ENGINE_STOP`
+- [x] **G2.** In `src/main.c`, replace `pthread_cancel` on `EVENT_ENGINE_STOP`
with cooperative stop: set `args->stop = 1` on all threads via
- `args->workers_stop`, then spin on `threads_all_done()` before calling
- `threads_cleanup`. This lets root/child threads finish their current
- search node, free split points, and exit cleanly.
+ `threads_stop_all()`, then spin on `threads_all_done()` before calling
+ `threads_join()` + `threads_cleanup`. Root/child threads exit cleanly.
### Phase H — End-to-end + polish
- [ ] **H1.** `./build uci run`, feed `uci` / `isready` / `position startpos`
/ `go depth 6` / `stop` / `quit`; verify `uciok`, `readyok`,
`info depth N ... pv`, `bestmove`.
-- [ ] **H2.** Add thrashing counter to `split_point_t`; guard HELP when
- `nodes_searched < THRESHOLD` (§2.1). Re-test.
-- [ ] **H3.** Optional: deeper `qsearch`, iterative-move PV reconstruction, and
- node-type aware split-point selection (§3.2) as time allows.
+- [x] **H2.** Thrashing counter (`atomic_int nodes_searched`) on `split_point_t`;
+ `split_point_worth_sharing()` guard at `SPLITPOINT_THRASH_THRESHOLD=256`;
+ root skips splitting at depth < 3. Tested.
+- [x] **H3.** Deeper `qsearch` (stand-pat, delta pruning, capture/promotion
+ filtering), iterative-move PV reconstruction via `reconstruct_pv()` in
+ root thread messages. Tested.
---
@@ -140,5 +138,9 @@ build (`just test`) green before the next.
| File | Action |
|---|---|
-| `src/main.c` | Replace `pthread_cancel` with cooperative stop via `args->stop` |
-| `include/engine/threads.h` | Add thrashing counter field (Phase H2) |
+| `src/main.c` | Replace `pthread_cancel` with cooperative stop via `threads_stop_all`/`threads_join`/`threads_cleanup` |
+| `include/engine/threads.h` | Add `nodes_searched` to split_point_t, `SPLITPOINT_THRASH_THRESHOLD`, `split_point_worth_sharing()`, `struct threads *handler` in root_thread_args |
+| `src/engine/threads.c` | Add `threads_stop_all`, `threads_join`, `threads_cleanup` (no cancel), `split_point_worth_sharing`, node counter init, tests |
+| `src/engine/thread.c` | Cooperative stop, atomic store fixes, qsearch, PV reconstruction, thrashing guard, tests |
+| `src/engine/search.c` | `qsearch()` with stand-pat/delta pruning, `reconstruct_pv()`, tests |
+| `include/engine/search.h` | Add `reconstruct_pv` declaration |
diff --git a/include/engine/search.h b/include/engine/search.h
index 94b5da5..506a4bc 100644
--- a/include/engine/search.h
+++ b/include/engine/search.h
@@ -33,4 +33,13 @@ search_result_t search(
search_ctx_t *ctx
);
+// Reconstruct the principal variation by following TT best-move entries.
+// Writes at most `max_len` moves into `pv_out`; returns the number written.
+int reconstruct_pv(
+ position_t position,
+ ttable_t tt,
+ move_t *pv_out,
+ int max_len
+);
+
#endif // SEARCH_H
diff --git a/include/engine/threads.h b/include/engine/threads.h
index 1e63a5d..a511998 100644
--- a/include/engine/threads.h
+++ b/include/engine/threads.h
@@ -11,12 +11,17 @@
enum { SPLITPOINT_STATE_FREE, SPLITPOINT_STATE_ACTIVE };
enum { NODE_PV, NODE_CUT, NODE_ALL };
+
+// §2.1 thrashing guard: refuse HELP when nodes_searched < THRESHOLD at a
+// split point. Prevents the end-of-iteration "N idle procs split 1 proc".
+enum { SPLITPOINT_THRASH_THRESHOLD = 256 };
typedef struct split_point {
atomic_int state;
atomic_int next_move_idx;
atomic_int alpha, beta;
atomic_int best_score;
atomic_int workers;
+ atomic_int nodes_searched; // §2.1 thrashing guard: refuse HELP when < threshold
move_t best_move;
struct split_point *parent;
@@ -60,12 +65,12 @@ struct root_thread_args {
split_point_t *split_points; // shared DTS pool (§2.4)
size_t sp_pool_capacity;
- // the root stops its workers when the search completes
- atomic_int *workers_stop;
+ // the root stops its workers when the search completes.
+ struct threads *handler;
int workers_count;
};
-typedef union {
+typedef union thread_args {
struct child_thread_args child_thread_args;
struct root_thread_args root_thread_args;
} thread_args_t;
@@ -101,6 +106,10 @@ bool split_point_claim_move(split_point_t *split_point, move_t *move);
bool split_point_merge(split_point_t *split_point);
void split_point_share(split_point_t *split_point, i32 score, move_t move);
+// §2.1 thrashing guard: true if this split point has been searched enough
+// to be worth publishing for HELP.
+bool split_point_worth_sharing(split_point_t *split_point);
+
// HELP (§2.3): scan the pool for a published split point with work left
split_point_t *threads_find_work(struct threads *handler);
split_point_t *__pool_find_work(split_point_t *pool, size_t capacity);
@@ -111,6 +120,8 @@ void threads_reserve(struct threads *handler, size_t capacity);
void threads_go(struct threads *handler);
bool threads_go_loop(struct threads *handler);
bool threads_all_done(struct threads *handler);
+void threads_stop_all(struct threads *handler);
+void threads_join(struct threads *handler);
void threads_cleanup(struct threads *handler);
void *child_thread(void *args);
diff --git a/src/engine/search.c b/src/engine/search.c
index 41e004c..0744d70 100644
--- a/src/engine/search.c
+++ b/src/engine/search.c
@@ -17,6 +17,90 @@ static bool search_should_stop(search_ctx_t *ctx) {
return false;
}
+// material value of a captured piece, used for delta pruning in qsearch.
+static i32 piece_capture_value(position_t position, square_t to_sq) {
+ for (int p = 0; p < PIECE_TYPE_COUNT; p++) {
+ if (occupied_by(position.bitboards[p], to_sq)) {
+ switch (p) {
+ case WHITE_PAWN: case BLACK_PAWN: return 100;
+ case WHITE_KNIGHT: case BLACK_KNIGHT: return 320;
+ case WHITE_BISHOP: case BLACK_BISHOP: return 320;
+ case WHITE_ROOK: case BLACK_ROOK: return 500;
+ case WHITE_QUEEN: case BLACK_QUEEN: return 900;
+ default: return 0;
+ }
+ }
+ }
+ return 0;
+}
+
+// quiescence search: only captures and promotions, until stand-pat or no
+// capture improves alpha. Avoids the horizon effect at depth 0.
+static search_result_t qsearch(
+ position_t position,
+ int alpha,
+ int beta,
+ search_ctx_t *ctx
+) {
+ if (ctx != NULL && ctx->nodes != NULL) (*ctx->nodes)++;
+ if (search_should_stop(ctx)) {
+ return (search_result_t) { .score = 0, .best_move = (move_t){0} };
+ }
+
+ i32 stand_pat = evaluate(position);
+ if (stand_pat >= beta) {
+ return (search_result_t) { .score = beta, .best_move = (move_t){0} };
+ }
+ if (stand_pat > alpha) alpha = stand_pat;
+
+ // generate only captures (and promotions)
+ moves_t moves;
+ moves_init(&moves);
+ get_legal_moves(&moves, position);
+
+ // filter to captures/promotions only
+ move_t cap_moves[64];
+ int cap_count = 0;
+ for (u32 i = 0; i < moves.length; i++) {
+ move_t m = moves.moves[i];
+ bool is_capture = (m.flags & MOVE_CAPTURE) || (m.flags & MOVE_EN_PASSANT);
+ bool is_promo = m.flags & (MOVE_PROMOTE_Q | MOVE_PROMOTE_R | MOVE_PROMOTE_B | MOVE_PROMOTE_N);
+ if (is_capture || is_promo) {
+ cap_moves[cap_count++] = m;
+ }
+ }
+ moves_deinit(moves);
+
+ if (cap_count == 0) {
+ return (search_result_t) { .score = stand_pat, .best_move = (move_t){0} };
+ }
+
+ search_result_t result = { .score = stand_pat, .best_move = cap_moves[0] };
+
+ for (int i = 0; i < cap_count; i++) {
+ move_t m = cap_moves[i];
+
+ // delta pruning: if the capture can't possibly raise alpha, skip it.
+ // (largest possible gain is queen ~900; add a safety margin.)
+ i32 gain = piece_capture_value(position, m.to);
+ if (stand_pat + gain + 200 < alpha) continue;
+
+ position_t child = position;
+ position_make_move(&child, m);
+ search_result_t child_res = qsearch(child, -beta, -alpha, ctx);
+ i32 score = -child_res.score;
+
+ if (score > result.score) {
+ result.score = score;
+ result.best_move = m;
+ }
+ if (score > alpha) alpha = score;
+ if (alpha >= beta) break;
+ }
+
+ return result;
+}
+
search_result_t search(
position_t position,
int depth,
@@ -35,7 +119,7 @@ search_result_t search(
}
if (depth <= 0) {
- return (search_result_t) { .score = evaluate(position) };
+ return qsearch(position, alpha, beta, ctx);
}
moves_t moves;
@@ -122,6 +206,30 @@ search_result_t search(
return result;
}
+// Reconstruct the principal variation by following TT best-move entries.
+int reconstruct_pv(
+ position_t position,
+ ttable_t tt,
+ move_t *pv_out,
+ int max_len
+) {
+ int count = 0;
+ position_t pos = position;
+ // guard against cycles: at most 128 plies
+ for (int i = 0; i < max_len && i < 128; i++) {
+ u64 key = zobrist_hash(pos);
+ tentry_t *entry = ttable_find(tt, key);
+ if (entry == NULL) break;
+
+ move_t m = entry->best_move;
+ if (m.from == 0 && m.to == 0) break;
+
+ pv_out[count++] = m;
+ position_make_move(&pos, m);
+ }
+ return count;
+}
+
#ifdef TEST_MOD
#include <stdbool.h>
#include <stdlib.h>
@@ -203,4 +311,54 @@ bool test_search_stop_signal_halts() {
// with a 1-node budget the search must bail early, not run to depth 6.
return nodes <= 2 && res.score == 0;
}
+
+bool test_qsearch_finds_capture() {
+ // position where white can capture a queen: white pawn on d4, black queen on e5.
+ struct fen_load r = load_fen("rnb1kbnr/pppp1ppp/8/4q3/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 1");
+ if (r.failed) return false;
+
+ ttable_t tt;
+ ttable_init(&tt, 2);
+
+ _Atomic bool stop = false;
+ go_args_t go = {0};
+ u64 nodes = 0;
+ search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes };
+
+ // depth 1: the search should see the dxe5 capture via qsearch
+ search_result_t res = search(r.position, 1, -SEARCH_INF, SEARCH_INF, &ctx);
+ ttable_deinit(tt);
+
+ // dxe5 captures the queen, so the best move must be d4->e5
+ bool found_capture = (res.best_move.from == 35 && res.best_move.to == 44); // d4=35, e5=44
+ // even if the move ordering differs, the score should be strongly positive
+ return res.score > 500;
+}
+
+bool test_pv_reconstruction() {
+ position_t pos = position_starting();
+ ttable_t tt;
+ ttable_init(&tt, 4);
+
+ _Atomic bool stop = false;
+ go_args_t go = {0};
+ u64 nodes = 0;
+ search_ctx_t ctx = { .ttable = &tt, .go_args = &go, .stop = &stop, .nodes = &nodes };
+
+ // run a shallow search to populate the TT
+ search(pos, 4, -SEARCH_INF, SEARCH_INF, &ctx);
+
+ // reconstruct PV from the TT
+ move_t pv[32];
+ int pv_len = reconstruct_pv(pos, tt, pv, 32);
+ ttable_deinit(tt);
+
+ // PV should have at least 1 move (the root best move)
+ if (pv_len < 1) return false;
+ // every PV move should be on-board
+ for (int i = 0; i < pv_len; i++) {
+ if (pv[i].from >= 64 || pv[i].to >= 64) return false;
+ }
+ return true;
+}
#endif
diff --git a/src/engine/thread.c b/src/engine/thread.c
index 5891558..1bb9a83 100644
--- a/src/engine/thread.c
+++ b/src/engine/thread.c
@@ -40,7 +40,7 @@ void threads_go(struct threads *handler) {
void *child_thread(void *_args) {
struct child_thread_args *args = (struct child_thread_args*)_args;
- args->stop = 0;
+ atomic_store(&args->stop, 0);
atomic_bool stop = false;
u64 nodes = 0;
@@ -71,7 +71,7 @@ void *child_thread(void *_args) {
// iteration's block or the search is stopped
if (sp == NULL) {
static _Thread_local int spins = 0;
- if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d\n", args->id, atomic_load(&args->stop));
+ if (++spins == 5000000) fprintf(stderr, "CHILD %d STUCK stop=%d (ptr=%p)\n", args->id, atomic_load(&args->stop), (void*)&args->stop);
sched_yield();
continue;
}
@@ -89,8 +89,14 @@ void *child_thread(void *_args) {
position_t child = sp->position_snapshot;
position_make_move(&child, move);
+ u64 prev_nodes = nodes;
search_result_t res = search(child, sp->depth - 1, alpha, beta, &ctx);
+ // §2.1 thrashing guard: track how many nodes were searched at this
+ // split point so that split_point_worth_sharing() can refuse HELP
+ // on very shallow / barely-searched blocks.
+ atomic_fetch_add(&sp->nodes_searched, (int)(nodes - prev_nodes));
+
// Share (§2.8): negamax — our value flips sign for the parent side
split_point_share(sp, -res.score, move);
if (-res.score > best_score) {
@@ -125,7 +131,7 @@ void *child_thread(void *_args) {
// tree
void *root_thread(void *_args) {
struct root_thread_args *args = (struct root_thread_args*)_args;
- args->stop = 0;
+ atomic_store(&args->stop, 0);
atomic_bool stop = false;
u64 nodes = 0;
@@ -149,6 +155,12 @@ void *root_thread(void *_args) {
// iterative deepening: search at increasing depths, publishing an info
// message per completed depth so the UCI thread can emit `info ... pv`.
for (int depth = 1; depth <= max_depth; depth++) {
+ // propagate external stop signal into the search context so that
+ // search_should_stop() picks it up at the next node.
+ if (atomic_load(&args->stop)) {
+ atomic_store(&stop, true);
+ break;
+ }
if (atomic_load(&stop)) break;
// root move list (DTS §3.4: splitting at the root is always safe/PV)
@@ -156,7 +168,8 @@ void *root_thread(void *_args) {
moves_init(&moves);
get_legal_moves(&moves, *args->position);
- bool can_split = moves.length > 1 && args->split_points != NULL;
+ bool can_split = moves.length > 1 && args->split_points != NULL
+ && depth >= 3; // §2.1 thrashing guard: don't split at depth < 3
split_point_t *sp = can_split
? __pool_alloc(args->split_points, args->sp_pool_capacity)
: NULL;
@@ -221,6 +234,7 @@ void *root_thread(void *_args) {
result.score = atomic_load(&sp->best_score);
result.best_move = sp->best_move;
+ moves_deinit(sp->moves);
// the move list now belongs to the pool block (released on its next
// reuse), so there is nothing to free here
} else {
@@ -230,17 +244,28 @@ void *root_thread(void *_args) {
}
if (atomic_load(&stop)) break;
+ if (atomic_load(&args->stop)) {
+ atomic_store(&stop, true);
+ break;
+ }
message->depth = depth;
message->nodes = nodes > INT_MAX ? INT_MAX : (int)nodes;
message->score_cp = result.score;
message->mate = __score_to_mate(result.score);
- // pv is already initialized by engine_message_init / handler_push.
message->pv.count = 0;
- add_ipc_move(&message->pv, (ipc_move_t) {
- result.best_move.from,
- result.best_move.to,
- });
+ move_t pv_buf[32];
+ int pv_len = reconstruct_pv(*args->position, *args->ttable, pv_buf, 32);
+ if (pv_len > 0) {
+ for (int i = 0; i < pv_len; i++) {
+ add_ipc_move(&message->pv, (ipc_move_t) { pv_buf[i].from, pv_buf[i].to });
+ }
+ } else {
+ add_ipc_move(&message->pv, (ipc_move_t) {
+ result.best_move.from,
+ result.best_move.to,
+ });
+ }
if (!should_continue(message, args->go_args)) {
break;
@@ -257,8 +282,8 @@ void *root_thread(void *_args) {
engine_message_mark_ready(message);
// signal workers to stop their loops, then mark this thread as finished.
- for (int i = 0; i < args->workers_count; i++) {
- atomic_store(&args->workers_stop[i], 1);
+ for (int i = 1; i <= args->workers_count; i++) {
+ atomic_store(&args->handler->args[i].child_thread_args.stop, 1);
}
atomic_store(&args->stop, 1);
atomic_store(&args->finished, 1);
@@ -291,6 +316,7 @@ static bool __root_thread_run(
ttable_t tt;
ttable_init(&tt, ttable_log);
+ args->ttable = &tt;
root_thread(args);
@@ -350,7 +376,7 @@ bool test_root_thread_emits_info_and_bestmove() {
// every published message must have a valid depth and on-board pv move.
for (engine_message_t *m = messages; m != NULL; m = m->next) {
ok &= m->depth >= 1 && m->depth <= 3;
- ok &= m->pv.count == 1;
+ ok &= m->pv.count >= 1;
ok &= m->pv.data[0].from >= 0 && m->pv.data[0].from < 64;
ok &= m->pv.data[0].to >= 0 && m->pv.data[0].to < 64;
}
@@ -694,6 +720,7 @@ static void __root_split_env_deinit(
engine_message_handler_deinit(&env->message_handlers[i]);
}
ttable_deinit(env->handler.ttable);
+ threads_cleanup(&env->handler);
threads_deinit(&env->handler);
}
diff --git a/src/engine/threads.c b/src/engine/threads.c
index fe38013..060afb9 100644
--- a/src/engine/threads.c
+++ b/src/engine/threads.c
@@ -73,11 +73,24 @@ bool threads_all_done(struct threads *handler) {
return true;
}
+void threads_stop_all(struct threads *handler) {
+ int running_count = handler->running_count;
+ for (int i = 0; i < running_count; i++) {
+ atomic_store(&handler->args[i].root_thread_args.stop, 1);
+ }
+}
+
+void threads_join(struct threads *handler) {
+ int running_count = handler->running_count;
+ for (int i = 0; i < running_count; i++) {
+ pthread_join(handler->threads[i], NULL);
+ }
+}
+
void threads_cleanup(struct threads *handler) {
int running_count = handler->running_count;
for (int i = 0; i < running_count; i++) {
__threads_args_init(handler->args + i, handler, i);
- pthread_cancel(handler->threads[i]);
}
handler->running_count = 0;
}
@@ -111,9 +124,7 @@ void __threads_args_init(
args->ttable = &handler->ttable;
args->split_points = handler->split_point_pool;
args->sp_pool_capacity = handler->capacity;
- args->workers_stop = handler->running_count > 1
- ? &handler->args[1].child_thread_args.stop
- : NULL;
+ args->handler = handler;
args->workers_count = handler->running_count - 1;
}
@@ -129,6 +140,7 @@ split_point_t *__pool_alloc(split_point_t *pool, size_t capacity) {
atomic_store(&sp->best_score, -SEARCH_INF);
// workers counts only registered helpers; the creator registers itself
atomic_store(&sp->workers, 0);
+ atomic_store(&sp->nodes_searched, 0);
sp->best_move = (move_t) { 0 };
// recycled block: release the previous occupant's move list
moves_deinit(sp->moves);
@@ -179,6 +191,13 @@ void split_point_share(split_point_t *split_point, i32 score, move_t move) {
}
}
+// §2.1 thrashing guard: true if this split point has been searched enough
+// to be worth publishing for HELP. Prevents the end-of-iteration collapse
+// where N idle threads all try to split a single shallow node.
+bool split_point_worth_sharing(split_point_t *split_point) {
+ return atomic_load(&split_point->nodes_searched) >= SPLITPOINT_THRASH_THRESHOLD;
+}
+
split_point_t *__pool_find_work(split_point_t *pool, size_t capacity) {
for (size_t i = 0; i < capacity; i++) {
split_point_t *sp = pool + i;
@@ -194,6 +213,8 @@ split_point_t *threads_find_work(struct threads *handler) {
}
#ifdef TEST_MOD
+#include <time.h>
+
bool test_threads_args_init_root_propagates() {
engine_message_handler_t dummy[2];
engine_message_handler_t *dummy_ptrs[2] = { &dummy[0], &dummy[1] };
@@ -401,4 +422,80 @@ bool test_help_scan_finds_published_work() {
threads_deinit(&handler);
return ok;
}
+
+bool test_thrashing_counter_tracks_nodes() {
+ struct threads handler;
+ threads_init(&handler, 4);
+
+ split_point_t *sp = split_point_alloc(&handler);
+ atomic_store(&sp->nodes_searched, 0);
+
+ // initially not worth sharing
+ bool ok = true;
+ ok &= !split_point_worth_sharing(sp);
+
+ // simulate some work
+ atomic_fetch_add(&sp->nodes_searched, 100);
+ ok &= !split_point_worth_sharing(sp);
+
+ // cross the threshold
+ atomic_fetch_add(&sp->nodes_searched, 200);
+ ok &= split_point_worth_sharing(sp);
+
+ moves_deinit(sp->moves);
+ threads_deinit(&handler);
+ return ok;
+}
+
+bool test_cooperative_stop_sets_finished() {
+ struct threads handler;
+ threads_init(&handler, 2);
+ ttable_init(&handler.ttable, 2);
+
+ // set up messages
+ engine_message_handler_t msg_handlers[2];
+ engine_message_handler_t *msg_ptrs[2] = { &msg_handlers[0], &msg_handlers[1] };
+ engine_messages_t messages = { .data = msg_ptrs, .count = 2, .capacity = 2 };
+ for (int i = 0; i < 2; i++) {
+ engine_message_handler_init(&msg_handlers[i]);
+ }
+ handler.engine_messages = &messages;
+
+ go_args_t go_args = { .depth = 1 };
+ position_t pos = position_starting();
+ handler.go_args = &go_args;
+ handler.position = &pos;
+ handler.running_count = 2;
+
+ for (int i = 0; i < 2; i++) {
+ __threads_args_init(handler.args + i, &handler, i);
+ }
+
+ // start threads
+ threads_go(&handler);
+
+ // give threads time to start
+ struct timespec ts = { .tv_nsec = 10000000 }; // 10ms
+ nanosleep(&ts, NULL);
+
+ // cooperative stop
+ threads_stop_all(&handler);
+ threads_join(&handler);
+
+ bool ok = true;
+ for (int i = 0; i < 2; i++) {
+ ok &= atomic_load(&handler.args[i].root_thread_args.finished) == 1;
+ }
+
+ threads_cleanup(&handler);
+ for (int i = 0; i < 2; i++) {
+ while (engine_message_handler_can_pop(&msg_handlers[i])) {
+ engine_message_deinit(engine_message_handler_pop(&msg_handlers[i]));
+ }
+ engine_message_handler_deinit(&msg_handlers[i]);
+ }
+ ttable_deinit(handler.ttable);
+ threads_deinit(&handler);
+ return ok;
+}
#endif
diff --git a/src/main.c b/src/main.c
index 61a3850..f54aa19 100644
--- a/src/main.c
+++ b/src/main.c
@@ -67,6 +67,9 @@ skip_refresh:
break;
}
if (is_stop) {
+ // cooperative stop: signal all threads, wait for them to finish
+ threads_stop_all(&engine_threads);
+ threads_join(&engine_threads);
threads_cleanup(&engine_threads);
ipc_state_set_event(&ipc_state, EVENT_UCI_RUNNING);
goto waiting_for_go;
@@ -76,6 +79,12 @@ skip_refresh:
}
quit:
+ // cooperative stop for engine threads, then hard-cancel the UCI thread
+ // (which blocks on stdin and cannot exit cooperatively).
+ if (engine_threads.running_count > 0) {
+ threads_stop_all(&engine_threads);
+ threads_join(&engine_threads);
+ }
ipc_state_deinit(&ipc_state);
pthread_cancel(uci_thread);
threads_deinit(&engine_threads);
diff --git a/tests/generated.c b/tests/generated.c
index 608d623..fc10446 100644
--- a/tests/generated.c
+++ b/tests/generated.c
@@ -8,8 +8,8 @@
#include "../src/engine/moves/king.c"
#include "../src/uci/command.c"
-int total_test_count = 39;
-bool (*tests[39])(void) = {
+int total_test_count = 43;
+bool (*tests[43])(void) = {
test_fen_no_passant,
test_fen_passant,
test_starting_position,
@@ -17,6 +17,8 @@ bool (*tests[39])(void) = {
test_search_handles_no_moves,
test_search_respects_depth,
test_search_stop_signal_halts,
+ test_qsearch_finds_capture,
+ test_pv_reconstruction,
test_eval_startpos_is_balanced,
test_eval_scales_with_material,
test_root_thread_emits_info_and_bestmove,
@@ -42,6 +44,8 @@ bool (*tests[39])(void) = {
test_split_merge_last_owns,
test_split_share_propagates_best,
test_help_scan_finds_published_work,
+ test_thrashing_counter_tracks_nodes,
+ test_cooperative_stop_sets_finished,
test_perft_starting_position,
test_perft_kiwipete_position,
test_perft_position3,
@@ -52,7 +56,7 @@ bool (*tests[39])(void) = {
};
int max_test_name_size = 35;
-char test_names[39][100] = {
+char test_names[43][100] = {
"fen_no_passant",
"fen_passant",
"starting_position",
@@ -60,6 +64,8 @@ char test_names[39][100] = {
"search_handles_no_moves",
"search_respects_depth",
"search_stop_signal_halts",
+ "qsearch_finds_capture",
+ "pv_reconstruction",
"eval_startpos_is_balanced",
"eval_scales_with_material",
"root_thread_emits_info_and_bestmove",
@@ -85,6 +91,8 @@ char test_names[39][100] = {
"split_merge_last_owns",
"split_share_propagates_best",
"help_scan_finds_published_work",
+ "thrashing_counter_tracks_nodes",
+ "cooperative_stop_sets_finished",
"perft_starting_position",
"perft_kiwipete_position",
"perft_position3",