-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Handle *some* out-of-spec behaviour explicitly #4563
base: master
Are you sure you want to change the base?
Conversation
No, for two reasons, 1) to the professional intensive care unit people, your You did not miss anything "important", as it is obvious that it is nothing complicated to filter out positions that will crash SF, however it is complicated to define what is allowed afterwards, do you allow for anything that passes your filter, or is it still "legal positions reachable from startpos"? If the former, are the conditions subject to change or permanent? |
src/position.cpp
Outdated
@@ -276,6 +317,9 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th | |||
// 5-6. Halfmove clock and fullmove number | |||
ss >> std::skipws >> st->rule50 >> gamePly; | |||
|
|||
if (st->rule50 > 100) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is hardly an issue since a user can work around this and submit a fen of counter 99, 2 plies earlier and then get his way through to the same fen we rejected.
On a different note If this is really relevant I think what should reject is negative values for integers instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that's possible. I think it depends on whether movegen generates an empty list when hitting rule50 or not, and I don't remember the behaviour there right now. If it generates moves normally then additional check would be required for the 'moves' part.
…that could result in too many generated moves. (for example 3 rooks and 9 queens)
You are not ignoring the error if you are sending an The fact that GUIs can currently not parse such a message is a valid concern. But I am sure that if SF commits to a particular error message format, GUIs using SF will start to parse it. Then others engines will follow the lead. The UCI protocol is not totally immutable and can evolve. For example UCI_Variant has been added in this way. |
Why do we suddenly care about usage that violates preconditions?
Crashing aside, this PR's purpose is to ensure there is no differences in observed and assumed state of the engine by the user - where sometimes input is ignored and sometimes everything can happen. |
Are you willing to specify the behaviour and state guarantees for every such case, for the other engines and GUIs to implement? |
Which precondition? I'm kinda lost in the chain of cause and consequences. |
The precondition that positions passed to the engine must be reachable from startpos |
And why are we starting to care about usage that violates it? |
you suggested such a use case as a reason to dismiss this PR |
If you argue that the outcome of such violations of precondition is not of concern, then I think we also won't need this PR to deal with the outcome of such violations of precondition to begin with. Whether it resulted in "happens to work" or "crashing" is irrelevant to the problem. |
I am not getting your point. SF could emit a message like
The GUI could recognize the "Invalid Position" part and inform the user of the error (possibly with the further explanations). In that way SF and a compliant GUI would not get out of sync. This is BTW the way the xboard protocol handles it. |
And what happens? What's the state after the error? |
I would presume that the program gracefully exits, or at least this is the most accepted opinion in #4558. To the GUI and user itself it won't make much of a difference though. |
Currently the GUI and the engine would be out of sync (which is not a fundamental problem since the GUI should not trust the engine output anyway, but it might be confusing for the user, although likely not since there is the I don't think this is currently a serious issue though since at this time GUIs should be somewhat conservative in the positions they send to UCI engines, since they have no way of knowing what positions the engine can handle. Indeed the party that knows best which positions an engine can handle is of course the engine itself and not the GUI. This is the philosophy of the xboard protocol and it makes a lot of sense IMHO. |
The best way to change the UCI protocol is to set the tone and return |
I think it is not a good idea to make an incompatible change. The GUIs will ignore the |
As I noted elsewhere, we already have similar actions relating to the net: Line 140 in 65e2150
|
To be very honest, I agree that removing undefined behavior is a good idea. I'm for this proposed change. However, might I suggest this be released as part of a major release, being a backwards compatibility breaking change (it can't be considered as a bug fix)? I'm not sure if Stockfish even cares about semantic versioning — but it's a nice thing for the end users. |
Stockfish doesn't use semantic versioning so there's no way to indicate a breaking change like that. It is also not a breaking change, in a sense, since inputs that would be experiencing a change would have been already violating the precondition. now. exit(EXIT_FAILURE) or std::terminate? |
However, such a change should be reflected in all parts of Stockfish's code. If that cannot be done and the other parts of the code use |
why not throw a runtime error? |
You mean without crashing? |
kills the program unless a try and catch block surrends it. |
Stockfish already can't guarantee quality analysis of such positions due to limitations imposed for performance reasons. Not allowing people to feed such positions to Stockfish and then have them make clickbait YouTube videos about how stockfish is blind is actually a good thing. Use fairy sf or stockfish mv for such positions, anyway. |
Probably this comment has been made before. Although UCI is supposed to be stateless, there is still a little bit of state introduced by the So it is seems to be ok that the engine, after receiving a People have argued (including me) that UCI mandates that the engine should simply ignore an invalid |
Moved the actual error handling to the UCI layer. This is close to the final form. Sanity check: https://tests.stockfishchess.org/tests/view/6461f8ae87f6567dd4df2ad8 I settle on crashing. Per UCI spec:
incorrect arguments result in undefined behaviour. The "ignore" clause only applies to commands. |
One little issue I have is that the final error message does not seem to say which command is actually the culprit of the error. I would prefer something like Although it seems the idea is regarded as controversial, I would still like to make it possible for the GUI to parse the error message in a simple way if it wants to do this (rather than just relaying the message to the user). |
It does have a somewhat granular error message, though for batched commands it could be even more precise. I'll see if it can be done in a reasonable way.
I'd like to see UCI specification first. Then we can implement it in Stockfish. |
Sure but the UCI specification is essentially frozen. The only way to make it move is to implement something reasonable and then hope it is copied (this is what happened with UCI_variant). Anyway my opinion on this is known. |
I'd like to avoid implementation defined behaviour The whole command that failed is now printed along the error message. |
Not a big deal, and maybe even a little bit nitpicky, but from my limited testing:
I think the explicit crash with the |
With debug=yes it leaks implementation details and suggests to the user it is a bug in Stockfish. |
I agree we shouldn't leak implementation details. I was thinking more of, for example, adding the "king count" part to the info string message rather than "it's in pos.is_ok()". Although I don't immediately see an easy way to change it. From the user's point of view, it seems inconsistent that some info string errors give details about what's wrong with the position, and some just say it's incorrect. |
|
||
const int wAdditionalKnights = std::max((int)count<KNIGHT>(WHITE) - 2, 0); | ||
const int bAdditionalKnights = std::max((int)count<KNIGHT>(BLACK) - 2, 0); | ||
const int wAdditionalBishops = std::max((int)count<BISHOP>(WHITE) - 2, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit: one could rather check difference of number of DSBs and LSBs to 1, for both white and black. (not sure how easy the number of dark squared bishops can be obtained, for example)
This would help catch a board with two white LSBs, no white DSB and 8 white pawns, say.
const int bAdditionalRooks = std::max((int)count<ROOK>(BLACK) - 2, 0); | ||
const int wAdditionalQueens = std::max((int)count<QUEEN>(WHITE) - 1, 0); | ||
const int bAdditionalQueens = std::max((int)count<QUEEN>(BLACK) - 1, 0); | ||
if (wAdditionalKnights + wAdditionalBishops + wAdditionalRooks + wAdditionalQueens > 8 - wPawns) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If one defines 8 - wPawns
as wMaxPromotions
, the latter could be set to zero if all opposing pawns are still on their original rank. (plus some other cases, where they form an impenetrable wall, that could never have been overcome by an opposing pawn). But these kind of complicated checks may already go beyond the original scope of this PR.
So, what's the consensus about this pull request? |
I think we should merge something along these lines, but was holding off until after the SF16 release, and busy afterwards. Probably, this could be revisited now as this gives some time to fix-up eventual fallout. In the back of my mind, I have been thinking also that there is possibly some overlap with #4626 or said differently, if we move in the direction of having a shared library of SF, we will have to think a bit more about error handling (i.e. a library should not abort). |
I agree with merging, though we should keep it rather simple in the future otherwise people will constantly try to add new rules to prohibit non legal chess positions. I think all we really care about is avoiding positions where a crash can happen (or which could later cause a crash). |
gamePly = 1; | ||
} | ||
|
||
// Technically, positions with rule50==100 are correct, just no moves can be made further. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the 50-move rule and 75-move rule do apply in over-the-board play, they do not apply for certain purposes in endgame theory. In particular, endgame tablebases will allow one to step through a forced with that takes far longer, and this would (presulably) result in a larger count here. Instead of refusing to import the FEN, I think it would be better for Stockfish to clamp rule50 to 150.
// gives any guarantees as to the program's behaviour for the user to rely on, | ||
// so the safest option is to terminate. | ||
sync_cout << "info string CRITICAL ERROR: Command `" << fullCommand << "` failed. Reason: " << message << '\n' << sync_endl; | ||
std::terminate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exiting is an appropriate action here, but I don’t think std::terminate()
is the right way to do it. std::terminate()
typically results in a core dump, so it should only be used when there is a bug in Stockfish itself. It isn’t the appropriate reaction to invalid input. In this case, I would just call exit()
if there are no other threads running, or quick_exit()
or _Exit
otherwise.
This is a preliminary set of changes to explicitly handle some cases that resulted in undefined behaviour previously:
pos_is_ok
checkAdditional validation changes:
TODO (at least what I have on mind right now, there's definitely more):
Any above errors are handled by calling
std::terminate
. The motivation for such behaviour is lack of specification from the UCI protocol. UCI allows ignoring errors, but ignoring errors never a good idea as it leads to differences between expected and actual state. Crashing is the only responsible thing we can do in this case.The test under https://tests.stockfishchess.org/tests/view/645b9b0a175801c38d5c4e85 was mostly a sanity check (with a slightly older version), I don't expect any performance degradation. The current state of this PR is that the changes are slapped on top of whatever was there. It may be better to rewrite some parts completely, but first I'd like to know if this is desired, and if I'm not missing something important.