1use std::{str::FromStr, time::Duration};
8
9use nom::{
10 branch::alt,
11 bytes::complete::{tag, take_until},
12 character::complete::{anychar, digit1, multispace0, multispace1},
13 combinator::{cut, eof, map, map_res, opt, recognize, value, verify},
14 multi::{many0_count, many1_count, many_till},
15 sequence::{delimited, pair, preceded},
16 Err, IResult,
17};
18
19use crate::{UciCommand, UciParseError, UciSearchOptions};
20
21#[cfg(feature = "types")]
22use crate::{uci_move, UciMove};
23
24#[cfg(feature = "validate-promotion-moves")]
25use nom::character::complete::char;
26#[cfg(feature = "validate-promotion-moves")]
27use nom::combinator::not;
28
29#[cfg(not(feature = "types"))]
30use nom::{character::complete::one_of, sequence::tuple};
31
32pub(crate) fn parse_uci_command(input: &str) -> Result<UciCommand, UciParseError> {
36 #[cfg(feature = "err-on-unused-input")]
37 let mut parser =
38 nom::combinator::all_consuming(map(many_till(anychar, parse_command), |(_, cmd)| cmd));
39
40 #[cfg(not(feature = "err-on-unused-input"))]
42 let mut parser = map(many_till(anychar, parse_command), |(_, cmd)| cmd);
43
44 parser(input).map(|(_rest, cmd)| cmd).map_err(|e| match e {
45 Err::Error(_) | Err::Incomplete(_) => UciParseError::UnrecognizedCommand {
47 cmd: input.to_string(),
48 },
49
50 Err::Failure(e) => {
52 let place = input.rfind(e.input).unwrap_or_default();
54
55 let place = if place < input.len() {
57 place.checked_sub(1).unwrap_or_default()
58 } else {
59 place
60 };
61
62 let cmd = input.get(..place).unwrap().to_string();
64
65 let arg = e.input.to_string();
67
68 if arg.is_empty() {
69 UciParseError::InsufficientArguments { cmd }
70 } else {
71 UciParseError::InvalidArgument { cmd, arg }
72 }
73 }
74 })
75}
76
77fn single_command<'a>(
79 ident: &'static str,
80 cmd: UciCommand,
81) -> impl FnMut(&'a str) -> IResult<&'a str, UciCommand> {
82 value(cmd, term(ident))
83}
84
85fn multi_command<'a, F>(
87 ident: &'static str,
88 parser: F,
89) -> impl FnMut(&'a str) -> IResult<&'a str, UciCommand>
90where
91 F: FnMut(&'a str) -> IResult<&'a str, UciCommand>,
92{
93 preceded(term(ident), cut(parser))
94}
95
96fn parse_command(input: &str) -> IResult<&str, UciCommand> {
98 alt((
99 single_command("uci", UciCommand::Uci),
100 multi_command("debug", parse_debug_args),
101 single_command("isready", UciCommand::IsReady),
102 multi_command("setoption", parse_setoption_args),
103 multi_command("register", parse_register_args),
104 single_command("ucinewgame", UciCommand::UciNewGame),
105 multi_command("position", parse_position_args),
106 multi_command("go", parse_go_args),
107 single_command("stop", UciCommand::Stop),
108 single_command("ponderhit", UciCommand::PonderHit),
109 single_command("quit", UciCommand::Quit),
110 #[cfg(feature = "parse-bench")]
111 multi_command("bench", parse_bench_args),
112 ))(input)
113}
114
115fn parse_debug_args(input: &str) -> IResult<&str, UciCommand> {
117 map(
118 alt((
119 value(true, term("on")), value(false, term("off")), )),
122 UciCommand::Debug,
123 )(input)
124}
125
126fn parse_setoption_args(input: &str) -> IResult<&str, UciCommand> {
128 let name = between("name", "value");
129 let value = rest_after("value");
130
131 map(pair(name, opt(value)), |(name, value)| {
132 UciCommand::SetOption {
133 name: name.to_string(),
134 value: value.map(str::to_string),
135 }
136 })(input)
137}
138
139fn parse_register_args(input: &str) -> IResult<&str, UciCommand> {
141 let later = value((None, None), term("later"));
143
144 let name = between("name", "code");
145 let code = rest_after("code");
146
147 map(
148 alt((
149 later, verify(pair(opt(name), opt(code)), |(n, c)| {
152 c.is_some() || n.is_some() }),
154 )),
155 |(name, code)| UciCommand::Register {
156 name: name.map(str::to_string),
157 code: code.map(str::to_string),
158 },
159 )(input)
160}
161
162fn parse_position_args(input: &str) -> IResult<&str, UciCommand> {
164 let fen = alt((
166 value(None, term("startpos")),
167 map(parse_fen, |s| Some(s.to_string())),
168 #[cfg(feature = "parse-position-kiwipete")]
169 value(
170 Some(String::from(
171 "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
172 )),
173 term("kiwipete"),
174 ),
175 ));
176
177 let moves = map(opt(moves_after("moves")), Option::unwrap_or_default);
178
179 map(pair(fen, moves), |(fen, moves)| UciCommand::Position {
180 fen,
181 moves: moves.into_iter().map(Into::into).collect(),
182 })(input)
183}
184
185fn parse_go_args(input: &str) -> IResult<&str, UciCommand> {
187 map(parse_search_options, UciCommand::Go)(input)
188}
189
190fn parse_search_options(input: &str) -> IResult<&str, UciSearchOptions> {
194 let mut opt = UciSearchOptions::default();
195
196 let (input, count) = many0_count(alt((
198 map(moves_after("searchmoves"), |x| {
199 opt.searchmoves = x.into_iter().map(Into::into).collect()
200 }),
201 map(term("ponder"), |_| opt.ponder = true),
202 map(time_after("wtime"), |x| opt.wtime = Some(x)),
203 map(time_after("btime"), |x| opt.btime = Some(x)),
204 map(time_after("winc"), |x| opt.winc = Some(x)),
205 map(time_after("binc"), |x| opt.binc = Some(x)),
206 map(num_after("movestogo"), |x| opt.movestogo = Some(x)),
207 map(num_after("depth"), |x| opt.depth = Some(x)),
208 map(num_after("nodes"), |x| opt.nodes = Some(x)),
209 map(num_after("mate"), |x| opt.mate = Some(x)),
210 map(time_after("movetime"), |x| opt.movetime = Some(x)),
211 map(term("infinite"), |_| opt.infinite = true),
212 #[cfg(feature = "parse-go-perft")]
213 map(num_after("perft"), |x| opt.perft = Some(x)),
214 )))(input)?;
215
216 if count == 0 {
218 opt.infinite = true;
219 }
220
221 Ok((input, opt))
222}
223
224#[cfg(feature = "parse-bench")]
226fn parse_bench_args(input: &str) -> IResult<&str, UciCommand> {
227 map(parse_search_options, UciCommand::Bench)(input)
228}
229
230fn term<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
234 delimited(multispace0, tag(ident), alt((eof, multispace1)))
235}
236
237fn rest_nonempty(input: &str) -> IResult<&str, &str> {
239 recognize(cut(many1_count(anychar)))(input)
240}
241
242fn between<'a>(ident: &'a str, end: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
246 preceded(
247 term(ident),
248 verify(
249 map(alt((take_until(end), rest_nonempty)), str::trim),
250 |s: &str| !s.is_empty(),
251 ),
252 )
253}
254
255fn rest_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str> {
258 preceded(term(ident), map(rest_nonempty, str::trim))
259}
260
261#[cfg(not(feature = "types"))]
263fn moves_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<&'a str>> {
264 preceded(
265 term(ident),
266 cut(nom::multi::many0(delimited(
267 multispace0,
268 recognize(uci_move),
269 multispace0,
270 ))),
271 )
272}
273
274#[cfg(feature = "types")]
275fn moves_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Vec<UciMove>> {
276 preceded(
277 term(ident),
278 cut(nom::multi::many0(delimited(
279 multispace0,
280 uci_move,
281 multispace0,
282 ))),
283 )
284}
285
286fn parse_fen(input: &str) -> IResult<&str, &str> {
290 between("fen", "moves")(input)
291}
292
293fn parse_num<T>(input: &str) -> IResult<&str, T>
297where
298 T: FromStr + Default + Clone,
299{
300 #[cfg(feature = "clamp-negatives")]
301 {
302 cut(alt((
303 map_res(digit1, str::parse), preceded(tag("-"), value(T::default(), digit1)), )))(input)
306 }
307
308 #[cfg(not(feature = "clamp-negatives"))]
309 cut(map_res(digit1, str::parse))(input)
310}
311
312fn num_after<'a, T>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, T>
316where
317 T: FromStr + Default + Clone,
318{
319 preceded(term(ident), parse_num)
320}
321
322fn parse_time(input: &str) -> IResult<&str, Duration> {
324 map(parse_num, Duration::from_millis)(input)
325}
326
327fn time_after<'a>(ident: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Duration> {
329 preceded(term(ident), parse_time)
330}
331
332#[cfg(not(feature = "types"))]
333fn uci_move(input: &str) -> IResult<&str, &str> {
340 #[inline(always)]
342 fn file(input: &str) -> IResult<&str, char> {
343 one_of("abcdefgh")(input)
344 }
345
346 #[inline(always)]
348 fn rank(input: &str) -> IResult<&str, char> {
349 one_of("12345678")(input)
350 }
351
352 #[inline(always)]
354 fn square(input: &str) -> IResult<&str, (char, char)> {
355 pair(file, rank)(input)
356 }
357
358 #[cfg(not(feature = "validate-promotion-moves"))]
359 {
360 let piece = one_of("PpNnBbRrQqKk");
362
363 recognize(alt((
364 recognize(tuple((square, square, opt(piece)))),
365 term("0000"),
366 )))(input)
367 }
368
369 #[cfg(feature = "validate-promotion-moves")]
370 {
371 #[inline(always)]
373 fn piece(input: &str) -> IResult<&str, char> {
374 one_of("QqNnRrBb")(input)
375 }
376
377 let non_promotion = recognize(tuple((square, square, not(piece))));
379
380 let promotion_rank1 =
382 recognize(tuple((pair(file, char('2')), pair(file, char('1')), piece)));
383
384 let promotion_rank8 =
386 recognize(tuple((pair(file, char('7')), pair(file, char('8')), piece)));
387
388 alt((
389 non_promotion,
390 promotion_rank1,
391 promotion_rank8,
392 term("0000"),
393 ))(input)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 fn new_cmd(input: &str) -> UciCommand {
403 let cmd = UciCommand::new(input);
404 assert!(
405 cmd.is_ok(),
406 "Failed to parse {input:?}\nGot {:?}",
407 cmd.unwrap_err()
408 );
409 cmd.unwrap()
410 }
411
412 fn new_err(input: &str) {
414 let cmd = UciCommand::new(input);
415 assert!(cmd.is_err(), "Should error from {input:?}\nGot {cmd:?}");
416 }
417
418 #[cfg(feature = "types")]
424 fn moves(moves: &[&str]) -> Vec<UciMove> {
425 moves.iter().map(|s| s.parse().unwrap()).collect()
426 }
427
428 #[cfg(not(feature = "types"))]
429 fn moves(moves: &[&str]) -> Vec<String> {
430 moves.iter().map(|s| s.to_string()).collect()
431 }
432
433 #[test]
434 fn test_parse_unknown_word() {
435 new_cmd("joho debug on");
436
437 new_err("joho");
438
439 new_err("debug joho on");
440
441 #[cfg(feature = "err-on-unused-input")]
442 new_err("debug on joho");
443
444 #[cfg(not(feature = "err-on-unused-input"))]
445 {
446 new_cmd("debug on joho");
447 }
448 }
449 #[test]
450 fn test_parse_single_commands() {
451 let cmd = new_cmd(" uci ");
452 assert_eq!(cmd, UciCommand::Uci);
453
454 let cmd = new_cmd(" \t\nuci \t \n");
455 assert_eq!(cmd, UciCommand::Uci);
456
457 let cmd = new_cmd(" isready ");
458 assert_eq!(cmd, UciCommand::IsReady);
459 }
460
461 #[test]
462 fn test_parse_debug() {
463 let cmd = new_cmd(" debug on");
464 assert_eq!(cmd, UciCommand::Debug(true));
465
466 let cmd = new_cmd(" debug off ");
467 assert_eq!(cmd, UciCommand::Debug(false));
468
469 new_err(" debug ");
470
471 new_err(" debug no");
472 }
473
474 #[test]
475 fn test_parse_setoption() {
476 let cmd = new_cmd("setoption name Nullmove value true\t");
477 assert_eq!(
478 cmd,
479 UciCommand::SetOption {
480 name: "Nullmove".into(),
481 value: Some("true".into())
482 }
483 );
484
485 let cmd = new_cmd("setoption name Selectivity value 3 ");
486 assert_eq!(
487 cmd,
488 UciCommand::SetOption {
489 name: "Selectivity".into(),
490 value: Some("3".into())
491 }
492 );
493
494 let cmd = new_cmd("setoption name Style value Risky ");
495 assert_eq!(
496 cmd,
497 UciCommand::SetOption {
498 name: "Style".into(),
499 value: Some("Risky".into())
500 }
501 );
502
503 let cmd = new_cmd("setoption name Clear Hash ");
504 assert_eq!(
505 cmd,
506 UciCommand::SetOption {
507 name: "Clear Hash".into(),
508 value: None,
509 }
510 );
511
512 let cmd = new_cmd(
513 "setoption name NalimovPath value c:\\chess\\tb\\4;c:\\chess\\tb\\5".into(),
514 );
515 assert_eq!(
516 cmd,
517 UciCommand::SetOption {
518 name: "NalimovPath".into(),
519 value: Some("c:\\chess\\tb\\4;c:\\chess\\tb\\5".into())
520 }
521 );
522
523 new_err("setoption");
524
525 new_err("setoption name");
526
527 new_err("setoption name value");
528
529 new_err("setoption name value Risky");
530
531 new_err("setoption name Clear Hash value");
532 }
533
534 #[test]
535 fn test_parse_register() {
536 let cmd = new_cmd("register later");
537 assert_eq!(
538 cmd,
539 UciCommand::Register {
540 name: None,
541 code: None
542 }
543 );
544
545 let cmd = new_cmd("register name Stefan MK code\t\t4359874324 \t");
546 assert_eq!(
547 cmd,
548 UciCommand::Register {
549 name: Some("Stefan MK".into()),
550 code: Some("4359874324".into())
551 }
552 );
553
554 let cmd = new_cmd("register name Stefan MK \t");
555 assert_eq!(
556 cmd,
557 UciCommand::Register {
558 name: Some("Stefan MK".into()),
559 code: None
560 }
561 );
562
563 let cmd = new_cmd("register code\t\t4359874324 \t");
564 assert_eq!(
565 cmd,
566 UciCommand::Register {
567 name: None,
568 code: Some("4359874324".into())
569 }
570 );
571
572 new_err("register");
573
574 new_err(" register name ");
575
576 new_err("register name code ");
577
578 new_err("register name Stefan MK code");
579 }
580
581 #[test]
582 fn test_parse_ucinewgame() {
583 let cmd = new_cmd(" ucinewgame ");
584 assert_eq!(cmd, UciCommand::UciNewGame);
585 }
586
587 #[test]
588 fn test_parse_position() {
589 let res =
590 parse_fen("fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1");
591 assert_eq!(
592 res.unwrap().1,
593 "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"
594 );
595
596 let res = parse_fen(
597 "fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 moves e2a6",
598 );
599 assert_eq!(
600 res.unwrap().1,
601 "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"
602 );
603
604 let res = moves_after("moves")("moves e2a6 \th3g2 h1g1 e6d5");
605 assert_eq!(res.unwrap().1, moves(&["e2a6", "h3g2", "h1g1", "e6d5"]));
606
607 let cmd = new_cmd("position startpos");
608 assert_eq!(
609 cmd,
610 UciCommand::Position {
611 fen: None,
612 moves: vec![]
613 }
614 );
615
616 let cmd = new_cmd("position fen r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 moves e2a6 \t h3g2 h1g1 e6d5");
617 assert_eq!(
618 cmd,
619 UciCommand::Position {
620 fen: Some(
621 "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1".into()
622 ),
623 moves: moves(&["e2a6", "h3g2", "h1g1", "e6d5"])
624 }
625 );
626
627 new_err(" position ");
628 new_err("position fen ");
629 new_err(" position \tfen\t moves\t");
630 new_err("\t position fen moves e2e4\t");
631 new_err("position moves\t");
632 new_err("\tposition\t moves e2e4 ");
633 }
634
635 #[test]
636 fn test_parse_num() {
637 let res = parse_num::<u64>("3141");
638 assert_eq!(res.unwrap().1, 3141);
639
640 let res = parse_num::<u8>("0");
641 assert_eq!(res.unwrap().1, 0);
642
643 let res = parse_num::<u32>("-");
644 assert!(res.is_err());
645
646 #[cfg(feature = "clamp-negatives")]
647 {
648 let res = parse_num::<u32>("-42");
649 assert_eq!(res.unwrap().1, 0);
650 }
651
652 #[cfg(not(feature = "clamp-negatives"))]
653 {
654 let res = parse_num::<u32>("-42");
655 assert!(res.is_err());
656 }
657 }
658
659 #[test]
660 fn test_parse_time() {
661 let res = parse_time("0");
662 assert_eq!(res.unwrap().1, Duration::from_millis(0));
663
664 #[cfg(feature = "clamp-negatives")]
665 {
666 let res = parse_time("-42");
667 assert_eq!(res.unwrap().1, Duration::from_millis(0));
668 }
669
670 #[cfg(not(feature = "clamp-negatives"))]
671 {
672 let res = parse_time("-42");
673 assert!(res.is_err());
674 }
675 }
676
677 #[test]
678 fn test_parse_go() {
679 let cmd = new_cmd("go");
680 assert_eq!(
681 cmd,
682 UciCommand::Go(UciSearchOptions {
683 infinite: true,
684 ..Default::default()
685 })
686 );
687
688 let cmd = new_cmd("go infinite");
689 assert_eq!(
690 cmd,
691 UciCommand::Go(UciSearchOptions {
692 infinite: true,
693 ..Default::default()
694 })
695 );
696
697 let cmd = new_cmd("go wtime 4000 btime 500 winc 60 binc 7");
698 assert_eq!(
699 cmd,
700 UciCommand::Go(UciSearchOptions {
701 wtime: Some(Duration::from_millis(4000)),
702 btime: Some(Duration::from_millis(500)),
703 winc: Some(Duration::from_millis(60)),
704 binc: Some(Duration::from_millis(7)),
705 ..Default::default()
706 })
707 );
708
709 let cmd = new_cmd("go ponder searchmoves e2e4 \t b6b7 nodes\t 42");
710 assert_eq!(
711 cmd,
712 UciCommand::Go(UciSearchOptions {
713 ponder: true,
714 searchmoves: moves(&["e2e4", "b6b7"]),
715 nodes: Some(42),
716 ..Default::default()
717 })
718 );
719
720 let cmd = new_cmd("go searchmoves e2e4 b6b7 \tponder\t\t wtime 10 btime 20\t winc 30 binc 40 movestogo 5 depth 6 nodes 7 mate 8 movetime 90 infinite ");
721 assert_eq!(
722 cmd,
723 UciCommand::Go(UciSearchOptions {
724 searchmoves: moves(&["e2e4", "b6b7"]),
725 ponder: true,
726 wtime: Some(Duration::from_millis(10)),
727 btime: Some(Duration::from_millis(20)),
728 winc: Some(Duration::from_millis(30)),
729 binc: Some(Duration::from_millis(40)),
730 movestogo: Some(5),
731 depth: Some(6),
732 nodes: Some(7),
733 mate: Some(8),
734 movetime: Some(Duration::from_millis(90)),
735 infinite: true,
736 #[cfg(feature = "parse-go-perft")]
737 perft: None,
738 })
739 );
740
741 #[cfg(feature = "err-on-unused-input")]
742 new_err(" go joho ");
743
744 new_err(" go movetime ");
745
746 new_err(" go movestogo ");
747
748 new_err(" go movestogo mate ");
749 }
750
751 #[test]
752 fn test_parse_errors() {
753 let unknown = "shutdown".parse::<UciCommand>();
754 assert!(matches!(
755 unknown.unwrap_err(),
756 UciParseError::UnrecognizedCommand { cmd: _ }
757 ));
758
759 let invalid = "position default".parse::<UciCommand>();
760 assert!(matches!(
761 invalid.unwrap_err(),
762 UciParseError::InvalidArgument { cmd: _, arg: _ }
763 ));
764
765 let insufficient = "setoption".parse::<UciCommand>();
766 assert!(matches!(
767 insufficient.unwrap_err(),
768 UciParseError::InsufficientArguments { cmd: _ }
769 ));
770 }
771
772 #[test]
773 #[cfg(feature = "types")]
774 fn test_parse_uci_move() {
775 use crate::*;
776 let input = "0000";
778 let res = uci_move(input);
779 assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
780 assert_eq!(
781 res.unwrap().1,
782 UciMove {
783 src: Square(File::A, Rank::One),
784 dst: Square(File::A, Rank::One),
785 promote: None
786 }
787 );
788
789 let input = "e2e4";
791 let res = uci_move(input);
792 assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
793 assert_eq!(
794 res.unwrap().1,
795 UciMove {
796 src: Square(File::E, Rank::Two),
797 dst: Square(File::E, Rank::Four),
798 promote: None
799 }
800 );
801
802 let input = "b7b8q";
804 let res = uci_move(input);
805 assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
806 assert_eq!(
807 res.unwrap().1,
808 UciMove {
809 src: Square(File::B, Rank::Seven),
810 dst: Square(File::B, Rank::Eight),
811 promote: Some(Piece::Queen)
812 }
813 );
814
815 #[cfg(not(feature = "validate-promotion-moves"))]
817 {
818 let input = "e2e4k";
819 let res = uci_move(input);
820 assert!(res.is_ok(), "Failed to parse {input:?}\nGot {res:?}");
821 assert_eq!(
822 res.unwrap().1,
823 UciMove {
824 src: Square(File::E, Rank::Two),
825 dst: Square(File::E, Rank::Four),
826 promote: Some(Piece::King)
827 }
828 );
829 }
830
831 #[cfg(feature = "validate-promotion-moves")]
833 {
834 let input = "e2e4k";
835 let res = uci_move(input);
836 assert!(res.is_err());
837 }
838 }
839}