jordanbray / chess

A rust library to manage chess move generation
https://jordanbray.github.io/chess/
MIT License
243 stars 55 forks source link

Hotfix for unsoundness issue with MoveGen (55) #60

Open terrorfisch opened 3 years ago

terrorfisch commented 3 years ago

See #55

I solved this by ignoring moves that do not fit in the list. The performance impact of this solution should be minimal because the branch predictor should be able to guess right and it should only introduce an additional conditional jump instruction. I did not measure anything though.

A clean fix would either require a SmallVec (probably best with a specialized happy path that does the same as above) or a ExpectedBoard or ValidBoard like type that upholds certain invariants.

jordanbray commented 3 years ago

This one is a bit tricky for me. This evil code-path is only possible if there are more pieces on the chess board than can exist on a chess board (or more than 2 e.p. captures possible at a time). Perhaps an assert on the number of pieces is cleaner? IE, don't use this if there are more than 16 pieces on the board.

Perhaps the assertion should be done in Board to keep the board "sane" at all times? Maybe in set_piece and friends? I'm not sure yet, let me think on this.

As far as I can tell, if that evil path is hit, something has already gone horribly wrong, and a panic may be in order.

Alternatively, there may be a cheap way to handle any number of pieces that I haven't thought of yet.

terrorfisch commented 3 years ago

I choose not to panic because I wanted to do a minimal change and panicking also increases the function size.

I'm not sure yet, let me think on this.

I also think that a final fix requires a few design choices on how to handle "custom" boards. I suggest merging this change for now because this is a potential security risk for instance if you have a server that allows loading custom boards.

jordanbray commented 3 years ago

I believe the following would resolve the unsoundness as well, but again I'd need to check more deeply:

If all the following is done in Board, I believe there would be no performance impact for chess engines, and minimal performance impact for other chess programs. I also believe this would ensure that no Board could exist in an invalid state. It's a tough invariant to hold in theory, but I do think this would about do it.

This upholds the same safety guarantees (as far as I can tell), but reduces the merge request down to 6-7 lines. It also completely removes the performance impact where it matters most.

terrorfisch commented 3 years ago

So your plan is to make it an safety relevant invariant that an object of type Board is always "sane"? One has to be very careful that it is impossible to break this invariant in safe code. This would require that Board::xor always does a runtime check or is marked as unsafe.

What are the invariants that you plan to guarantee? I would only choose a subset of the extended "sane" to keep upholding it easier an allow invalid pinned and checkers states:

Maybe we could factor out pieces, color_combined and combined into a seperate struct that always upholds these?

Note: One could replace piece_on with this

if self.combined() & opp == EMPTY {
    None
} else {
    let result: u8 = self.pieces.iter().enumerate().skip(1)
        .map(|(idx, bb)| (idx as u8) * ((opp & bb != EMPTY) as u8)).sum();

    Some(match result {
            0 => Piece::Pawn,
            1 => Piece::Knight,
            2 => Piece::Bishop,
            3 => Piece::Rook,
            4 => Piece::Queen,
            5 => Piece::King,
            _ => unreachable!()
    })
}

Or this completely branchless version

let result_plus_one: u8 = self.pieces.iter().enumerate()
  .map(|(idx, bb)| (idx as u8 + 1) * ((opp & bb != EMPTY) as u8)).sum();

Some(match result_plus_one.wrapping_sub(1) {
        0 => Piece::Pawn,
        1 => Piece::Knight,
        2 => Piece::Bishop,
        3 => Piece::Rook,
        4 => Piece::Queen,
        5 => Piece::King,
        _ => return None
})
jordanbray commented 3 years ago
terrorfisch commented 3 years ago

I am currently hacking together a solution that works via a destinct type which represents the "physical" board and handles to pieces and squares to avoid duplicate checks.

For safety purposes I can maybe agree, but that feels like a semantic argument because it still likely indicates a bug

Definitely but this is IMO a completely different category. If one has a safety relevant invariant there should go more effort into proving it.

On piece_on, That's pretty clever. I'd be curious about benchmarks. I did (a long, long time ago) write a branchless version, but the branched version was faster in practice (as measured by perft). Mine didn't look as nice though.

Thanks. I expect that the benchmark result depends heavily on the work load, the CPU architecture and the compiler (version).

jordanbray commented 3 years ago

Thanks. I expect that the benchmark result depends heavily on the work load, the CPU architecture and the compiler (version).

Yeah, benchmarks are very tricky. You gotta be very careful in order to get numbers. That said, I think I could probably get some numbers on that tonight.