Skip to content

refactor(eval): Move each leaf term into a module of its own under eval/ - #249

Merged
aywrite merged 1 commit into
masterfrom
eval/term-modules
Sep 13, 2026
Merged

aywrite merged 1 commit into
masterfrom
eval/term-modules

Conversation

@aywrite

@aywrite aywrite commented Sep 13, 2026

Copy link
Copy Markdown
Owner

A term was four places: its counts and masks in board.rs, its weights and fold in eval.rs, its slot arithmetic in tune.rs and the same arithmetic again in scripts/tune.py. Adding one meant editing all four and getting them to agree.

eval.rs becomes eval/, with a file per term. mobility.rs, shelter.rs and pawn_structure.rs each own their counts, masks, weights with the fit that produced them, fold, cache where they have one, and tests. mod.rs keeps the material values, the phase weights, the accumulator and the sum. A TERMS table in mod.rs describes each term once, tune.rs lays its slots out from it, and the terms run prints that layout as a second header line, which scripts/tune.py now reads instead of holding ten constants of its own. The search carries one eval::Caches, and eval and eval_cached share one body over a memo trait with two implementations. board.rs opens pub(crate) readers on the piece boards and the king's square and names no evaluation term.

Node counts are identical by construction. Bench 3868987 at depth 7, both pinned tables unchanged, EXPECTED_PASSES and EXPECTED_POINTS unchanged. Release tests 457 to 460, python 209 to 212. speed.sh read +0.3% at a 3.1% spread, inside the noise.

Four judgment calls, in the commit body: files_of lives beside spread in pawn_structure.rs; shelter_key moved with the term as shelter::key; the mobility count reads the knight table through a knight_attacks reader; the pawn span test stayed in board.rs.

Reviewed by a second agent that rebuilt both sides and compared the terms output over 787 positions (byte identical apart from the layout line). Its findings are folded in.

🤖 Generated with Claude Code

@aywrite

aywrite commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

Review round 1, folded into 87bab7d.

A second agent rebuilt both sides in its own target directory: 3868987 nodes on both, all 18 bench rows identical in every column, 457 to 460 release tests, the three ignored pins passing, clippy and fmt clean. It compared the terms output over 787 positions (bench, tactics.epd, strategy.epd): byte identical apart from the layout line, which covers the whole 812 slot weight vector and every coefficient. The new tune.py agreed with the old on is_material for all 812 slots, on frozen_slots for all 32 hold combinations, and on bounds_hold over three vectors. Every new test was mutation tested and fired.

Findings, none behavioural:

  • the commit body said tune.py dropped seven constants; it dropped ten
  • the width test's tail assertion could never fire, since the helper is handed a slice of exactly its width; it now asserts every entry within the width was written, and fails with its own message when a helper under-writes
  • tune.rs still named eval.rs
  • two board.rs comments named an evaluation term, against the claim that board.rs names none; reworded from the board's side
  • the same idiom twice in one file, in five places after the move and six more the sweep found
  • two single-term cache tests sat in mod.rs and held the cache fields open; moved into their term's file, fields private again
  • a test for a row whose id opens with the layout word, beside the one for the header word

Three new rust tests and three new python ones. Airgap and trailers checked.

eval.rs held three terms and everything they share, and board.rs held the
counts all three are read from. So a term was four places: its counts and
masks in board.rs, its weights and fold in eval.rs, its slot arithmetic in
tune.rs and the same arithmetic again in scripts/tune.py. Adding one meant
editing all four and getting them to agree, and nothing said when they did
not.

eval.rs becomes eval/, with a file per term. mobility.rs, shelter.rs and
pawn_structure.rs each own their counts, their masks, their weights with the
fit that produced them, their fold, their memo where they have one, and the
tests that pin them. The doc blocks explaining each fit move whole. mod.rs
keeps the material values, the phase weights, the accumulator and the sum.

The slot layout follows from one table. TERMS in eval/mod.rs is a descriptor
per term: its name, how many counts it is measured in per side and per half of
the taper, its weights and its counts. tune.rs reads its slot arithmetic off
that, so SLOTS is a const fn over the table, weight() walks it instead of a
nine branch ladder, and the three loops in Terms::of become one. The run
prints the result as a second header line, `layout midgame 384 endgame 384
material 6 mobility 4 shelter 7 pawn_structure 8`, and scripts/tune.py derives
its slots from that line rather than from ten constants of its own. Its five
guards against earlier layouts go with them: a run this file cannot fit is now
refused by the name of the term it does not price, which is the case those
lengths were standing in for. A header with no layout line is refused too.

The search carries one eval::Caches rather than two fields, and the sum is
written once over a private memo trait with two implementations, the caches
and a unit that recomputes. eval and eval_cached are the two doors on to it.

board.rs opens pub(crate) readers on the six piece boards and the king's
square, gains sides(color) for the (ours, theirs) match that was written four
times, and names no evaluation term at all.

Four judgment calls. files_of lives in pawn_structure.rs beside spread, which
is its inverse, and shelter.rs reads it from there; the alternative was a
bitboard helper in mod.rs, which is not what mod.rs is for. shelter_key moved
with the term as shelter::key, which is what leaves board.rs naming no term,
and it needed ZOBRIST to go pub(crate). The mobility count reads the knight
table through a new knight_attacks reader rather than the whole attack mask
table being opened. And the pawn span test stayed in board.rs, since what it
pins is a board helper against the generator's own masks rather than any
count.

Node counts are identical by construction: the arithmetic is the same
arithmetic in a different place. The bench is 3868987 at depth 7, both pinned
node count tables are unchanged, and so are EXPECTED_PASSES and
EXPECTED_POINTS. The release run goes from 457 tests to 460 and the python
ones from 210 to 213; the three new rust tests say that every term writes the
counts its width claims, that the table names the terms the sum adds and that
the run prints the layout its slots are laid out from. Two of the three new
python ones refuse a header with no layout line and a term with no bound; the
third keeps a row whose id opens with the layout line's own word.

Bench: 3868987
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Speed against 0b7c6245

Both sides built and run on this runner in this job, the way
scripts/speed.sh measures a perf commit. The change is between
medians, and the spread beside it is what the change has to be
read against. The node counts are the search's: they move when
the search does, and a speed change leaves them alone.

round     base nps  candidate nps
    1     13230246       14674096
    2     12928470       13158342
    3     13077042       13950087
    4     14653477       14275545
    5     15313017       15034008
    6     15134335       13362161
    7     15281385       14954514
    8     13402431       13346903
    9     12979780       13155702

             nodes    time  median nps  fastest nps
base       3868987  0.29 s    13402431     15313017
candidate  3868987  0.28 s    13950087     15034008
change                           +4.1%        -1.8%

the change is inside the spread, so the medians make no claim; the
fastest column is the steadier comparison when the machine was not
quiet

Speed: +4.1% (bench nps, 9 interleaved rounds vs 0b7c6245, spread 17.8%)

@aywrite
aywrite merged commit 378c148 into master Sep 13, 2026
19 of 20 checks passed
@aywrite
aywrite deleted the eval/term-modules branch September 13, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant