← board

Rust corpus: the own-written chess engine as Track R's real-world target

STATUS (2026-07-16)

Adapted-branch engine: DONE and bit-comparable by perft. A pxx-friendly branch of the movegen/search compiles and runs, and its move enumeration is identical to the reference perft — the "bit-comparable" bar for a chess engine. Two forms live in test/:

Frontend enablers landed to get here (all green, self-host byte-identical): 5/6-param internal calls (r8/r9 spill), fixed arrays of structs (arr[i].field), slice-of-record (&[Move], slice[i].field).

NOT done: the UNMODIFIED ~/nextlevel/engine/src sources. Those remain blocked on value-flow features the adapted branch sidesteps — in priority order as the real modules hit them: Option<T> (chess.rs wall, stage 2) DONE 2026-08-29, array-typed STRUCT FIELDS (squares: [Piece; 64]) DONE 2026-08-29, array-typed return values (fn -> [T; N], attacks.rs) DONE 2026-08-29, if as an EXPRESSION (let x = if c { a } else { b }; — not on the original list) DONE 2026-08-29, the unity build for data modules (tables.rs, stage 3) DONE 2026-08-29, Result/? DONE 2026-08-29, then String/format!, derives/traits. Do NOT claim the real source compiles — only the adapted branch does.

Note on where the engine sources live: they are NOT on the frank-rust box (~/nextlevel does not exist here), so stage-2 work was driven from the gap list above and from the shapes the real modules are written in, not from re-probing the source. Re-probe before claiming a rung against the real files.

Why this target is ideal

Baseline (2026-07-09, rparser as of the ports-back pass)

Every module dies within its first 4 lines: use items unhandled, #[derive(...)]/#[inline] attributes unhandled, top-level const X: usize = ... items unhandled, pub type aliases unhandled. So stage 0 is pure swallowing/trivia, cheap and high-leverage.

Staged plan (each stage = more of the engine parses/compiles)

  1. Trivia sweep (cheap): swallow use ...;, #[...]/#![...], //! docs; pub type Alias = ...;; top-level const NAME: T = expr; including const arrays ([T; N] literals already landed at let-level).
  2. Core-language pass (single file): tuple structs (Square(pub u8)), Self in impls, method calls with by-value self returning Self, match on Self::Variant paths, u8/i8 arithmetic with as casts, wrapping_add/sub/shr mapped to plain ops (documented deviation).
  3. Option + str (drives [[feature-rust-rtl-core-types]] and the &str half of [[feature-rust-borrowed-slice-type]]): Option<T> as a monomorphized generic enum (concrete enums + generic fns both exist; generic ENUM instantiation is the new piece), &str as the landed ptr+len slice with .len()/.as_bytes()/byte indexing.
  4. Modules via unity build (kills the multi-file problem the zlib way): a runner.rs concatenation (or a tiny preprocessor step stripping use crate::... and mod x;) — no real module system needed, same trick as test/zlib/runner.c.
  5. ArrayVec replacement: pxx-friendly engine branch with a local struct MoveList { data: [Move; 256], len: usize } — allowed because the target is ours.
  6. Traits/derives as used ([[feature-rust-derive-macros]], [[feature-rust-dyn-trait-dispatch]]): the engine mostly needs PartialEq/Clone/Copy derives (field-wise synthesis) and fmt::Display for UCI output — the latter may be cheaper rerouted through println!-style intrinsics than through real trait dispatch.
  7. Gate ladder: all files parse → chess.rs compiles → perft(4) matches cargo → search finds a mate-in-2 → uci.rs echo loop.

Non-goals

Log

Stage 4 (2026-08-29): the ArrayVec rewrite — the last ladder item, DONE

The engine no longer opens each search node with

let mut mv: [Move; 256];      // uninitialised -- pxx accepts, rustc rejects
let ms = &mv[0..256];
let n = gen_moves(b, side, ep, castle, ms);

and no longer threads the count by hand through fn add(ms: &[Move], n: i64, ..) -> i64. Both are gone, replaced by the shape the real source uses:

struct MoveList { data: [Move; 256], len: i64 }
impl MoveList {
    fn new() -> MoveList { MoveList { data: [Move { from: -1, to: -1, flags: 0 }; 256], len: 0 } }
    fn push(&mut self, from: i64, to: i64, flags: i64) { .. }
    fn get(&self, i: i64) -> Move { self.data[i] }
    fn len(&self) -> i64 { self.len }
}

gen_moves takes &mut MoveList and returns nothing; the three search loops (perft, negamax, best_move) read slots back with ml.get(i).

One frontend rung was needed and only one[Name { .. }; N], a repeat array whose element is a struct literal, in both the let and the struct-field position (rung 16, feature-rust-option-type). Everything else the shape wants was measured working before anything was written: array-of-struct fields, self.data[self.len].from = f through &mut self, fn get(&self, i) -> Move, fn f(ml: &mut MoveList) with f(&mut ml), and whole-record copies between array slots. This ticket's earlier note that "list[i] = some_move (record-value copy) is not yet wired" was stale and is retracted.

Cost: 0.252s vs 0.192s for the same perft(4)+search workload — the 255 constructor copies plus the ~6 KB by-value MoveList return, once per search node (~9.3k nodes). Real Rust would NRVO both away; pxx does not, and at this size it does not matter. Recorded so nobody re-measures it as a regression.

Two pre-existing Track R bugs fell out of the probing and are filed, not fixed: bug-rust-slice-param-fn-erases-mains-record-array-element-type (a fn with a slice param and arity >= 2 before main makes a [Struct; N] local in main lose its element record — a parse error for a struct element, a segfault when the slice's element is that same struct) and bug-rust-whole-array-borrow-as-a-slice-argument-segfaults (f(&arr) compiles clean and crashes). The engine dodges the first because main's only arrays are scalar boards, and the second because it binds let b = &board[0..64]; first.

The file is still not rustc-clean, and the header says so honestly rather than claiming otherwise: b[from] indexes a slice with an i64 (rustc wants usize), and the boards are mutated through &[i64] rather than &mut [i64]. What changed is that the move list no longer has a form with no Rust meaning at all.