As a Rust beginner (about two months in), I've been developing a chess engine to learn the language. I have substantial experience implementing chess engines in other languages like C++, but I'm committed to finding truly idiomatic Rust solutions rather than just translating my C++ approach.
I've hit a challenging design problem involving Rust's borrowing rules, and I'm looking for guidance on the most elegant solution.
Current Structure
My engine has two key components:
A Position struct that represents the chess board state
A Search struct that contains a mutable reference to this position
The search method works recursively: it plays a move, calls itself recursively to explore deeper positions, then undoes the move when returning. This ensures that despite mutations, the position always reverts to its original state after each recursive call completes.
The Problem
I need a MoveGenerator struct that:
Efficiently generates legal moves from a position
Generates moves lazily (in stages) for performance reasons
Needs to reference the Position object when its next() method is called
The MoveGenerator works correctly as long as the position is in the same state when next() is called as it was during the generator's creation. My search algorithm guarantees this invariant, but Rust's borrowing rules prevent this design because the position is being mutated between calls.
Cloning the position is not an option due to performance requirements.
Solutions I've Considered
Using Rc<RefCell<Position>>: This appears to be the idiomatic solution, but it:
Complicates the search function with explicit borrowing/dropping
Causes a 3% performance penalty (significant for chess engines where every bit of speed matters)
Passing a position reference to next(): Instead of storing the reference in MoveGenerator, I could pass it each time. This seems inefficient since the reference is usually unnecessary after initial move generation.
Using UnsafeCell: This might offer zero overhead but introduces unsafe code, which I'd prefer to avoid if there's a better solution.
My Question
Is there an idiomatic Rust pattern I'm missing that would maintain both safety and performance? How would experienced Rust developers approach this problem without compromising on either aspect?
It's hard to follow exactly what your current situation is, when described like this. In particular, itβs unclear how Search fits in β you explain search() but not Search. Can you please post code, including at least the following?
The definitions of all structs that have lifetime parameters.
The signatures (including enclosing impl block signatures) of all the functions involved.
Compilable code (even if the actual algorithms are stubbed out with unimplemented!()) will especially help because it lets us test out proposed solutions.
You said that Search would contain a mutable reference to Position but here it owns a Position. Is that correct? Those are very different.
Thinking about the structure of the recursion, I see two straightforward approaches:
Avoid using &mut self in the recursive search function; pass the Position in use separately from the depth. Replace self.position.make(mv) with move_generator.position.make(mv).
Get rid of all mutable references, and store a Cell<Position> instead of a Position, which you can mutate through &. (If your Position type isn't Copy, then it should be. You're using bitboards, right?) This way, you can mutate in patterns that aren't expressible by mutable borrows, and there is no overhead of RefCell checking.
If I understand correctly, you propose that the position is passed to search() as a reference and that reference is used to create the move_generator. However if the move generator keeps a reference to the position for its lifetime can I still make a recursive call to search and passing a refernce to the position?
My goal is to have only one position (no copy/clone) and at each depth of the recusives call to search a move_generator also has a reference to the same position.
I will test it. If the overhead is minimal this will be my solution even if it is not completely clean. Or maybe it is an elagant solution and I don't realise it because I'm used to other languages.
It is possible to express a position using an array of only 32 bytes (i.e. four bits per square), which should be not too expensive to clone.
Alternatively, a position can be expressed using a reference to the previous position plus the last move. This way, you can traverse the search tree while cheaply creating new positions and discarding them as you return.
It is difficult to do that efficiently - better to calculate all the pseudo legal moves in one go even if you end up not needing all of them. That way you can also sort the moves by value which will help the search (pruning).