1use std::{borrow::Borrow, collections::BTreeSet, fmt, ops::Range, str::FromStr};
2
3use regex::Regex;
4
5use litcheck::diagnostics::{Diagnostic, SourceSpan, Span, Spanned};
6
7use crate::config::FeatureSet;
8
9#[derive(Debug, Diagnostic, thiserror::Error)]
11pub enum InvalidBooleanExprError {
12 #[error("unexpected token")]
13 #[diagnostic(help("expected one of: {}", .expected.join(", ")))]
14 UnexpectedToken {
15 #[label("{token} is not valid here")]
16 span: SourceSpan,
17 token: String,
18 expected: Vec<&'static str>,
19 },
20 #[error("unexpected character")]
21 #[diagnostic()]
22 UnexpectedChar {
23 #[label("'{c}' is not valid here")]
24 span: SourceSpan,
25 c: char,
26 },
27 #[error("unexpected end of expression")]
28 #[diagnostic(help("expected one of: {}", .expected.join(", ")))]
29 UnexpectedEof {
30 #[label("occurs here")]
31 span: SourceSpan,
32 expected: Vec<&'static str>,
33 },
34 #[error("invalid regex")]
35 #[diagnostic()]
36 InvalidRegex {
37 #[label("{error}")]
38 span: SourceSpan,
39 #[source]
40 error: regex::Error,
41 },
42}
43impl InvalidBooleanExprError {
44 pub fn with_span_offset(mut self, offset: usize) -> Self {
45 match &mut self {
46 Self::UnexpectedToken { ref mut span, .. }
47 | Self::UnexpectedChar { ref mut span, .. }
48 | Self::UnexpectedEof { ref mut span, .. }
49 | Self::InvalidRegex { ref mut span, .. } => {
50 let start = span.start() + offset;
51 let end = span.len() + start;
52 *span = SourceSpan::from(start..end);
53 self
54 }
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub enum BooleanExpr {
62 Lit(bool),
64 Var(String),
68 Pattern(Regex),
70 Not(Box<BooleanExpr>),
72 And(Box<BooleanExpr>, Box<BooleanExpr>),
74 Or(Box<BooleanExpr>, Box<BooleanExpr>),
76}
77impl Default for BooleanExpr {
78 #[inline(always)]
79 fn default() -> Self {
80 Self::Lit(false)
81 }
82}
83impl FromStr for BooleanExpr {
84 type Err = InvalidBooleanExprError;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 let parser = BooleanExprParser::new(s);
88 parser.parse()
89 }
90}
91impl std::convert::AsRef<BooleanExpr> for BooleanExpr {
92 fn as_ref(&self) -> &BooleanExpr {
93 self
94 }
95}
96impl Eq for BooleanExpr {}
97impl PartialEq for BooleanExpr {
98 fn eq(&self, other: &Self) -> bool {
99 match (self, other) {
100 (Self::Lit(x), Self::Lit(y)) => x == y,
101 (Self::Var(x), Self::Var(y)) => x == y,
102 (Self::Pattern(x), Self::Pattern(y)) => x.as_str() == y.as_str(),
103 (Self::Not(x), Self::Not(y)) => x == y,
104 (Self::And(xl, xr), Self::And(yl, yr)) => xl == yl && xr == yr,
105 (Self::Or(xl, xr), Self::Or(yl, yr)) => xl == yl && xr == yr,
106 (_, _) => false,
107 }
108 }
109}
110impl fmt::Display for BooleanExpr {
111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112 match self {
113 Self::Lit(x) => write!(f, "{x}"),
114 Self::Var(x) => write!(f, "{x}"),
115 Self::Pattern(x) => write!(f, "{{{{{}}}}}", x.as_str()),
116 Self::Not(ref x) => write!(f, "!{x}"),
117 Self::And(ref x, ref y) => write!(f, "({x} && {y})"),
118 Self::Or(ref x, ref y) => write!(f, "({x} || {y})"),
119 }
120 }
121}
122impl<'de> serde::Deserialize<'de> for BooleanExpr {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: serde::Deserializer<'de>,
126 {
127 use serde::de::Visitor;
128
129 struct BooleanExprVisitor;
130 impl<'de> Visitor<'de> for BooleanExprVisitor {
131 type Value = BooleanExpr;
132
133 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
134 formatter.write_str("a valid boolean feature expression")
135 }
136
137 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
138 where
139 E: serde::de::Error,
140 {
141 s.parse::<BooleanExpr>().map_err(serde::de::Error::custom)
142 }
143 }
144
145 deserializer.deserialize_str(BooleanExprVisitor)
146 }
147}
148impl BooleanExpr {
149 pub fn evaluate<E: Environment>(&self, env: &E) -> bool {
155 match self {
156 Self::Lit(value) => *value,
157 Self::Var(ref id) => env.is_defined(id.as_str()),
158 Self::Pattern(ref pattern) => env.has_matching_definition(pattern),
159 Self::Not(ref expr) => !expr.evaluate(env),
160 Self::And(ref lhs, ref rhs) => lhs.evaluate(env) && rhs.evaluate(env),
161 Self::Or(ref lhs, ref rhs) => lhs.evaluate(env) || rhs.evaluate(env),
162 }
163 }
164}
165
166pub trait Environment {
167 fn is_defined(&self, name: &str) -> bool;
168 fn has_matching_definition(&self, pattern: &Regex) -> bool;
169}
170impl<T: Borrow<str>> Environment for Vec<T> {
171 #[inline]
172 fn is_defined(&self, name: &str) -> bool {
173 self.iter().any(|v| v.borrow() == name)
174 }
175 #[inline]
176 fn has_matching_definition(&self, pattern: &Regex) -> bool {
177 self.iter().any(|v| pattern.is_match(v.borrow()))
178 }
179}
180impl<T: Borrow<str>> Environment for &[T] {
181 #[inline]
182 fn is_defined(&self, name: &str) -> bool {
183 self.iter().any(|v| v.borrow() == name)
184 }
185 #[inline]
186 fn has_matching_definition(&self, pattern: &Regex) -> bool {
187 self.iter().any(|v| pattern.is_match(v.borrow()))
188 }
189}
190impl Environment for FeatureSet {
191 #[inline]
192 fn is_defined(&self, var: &str) -> bool {
193 self.contains(var)
194 }
195 #[inline]
196 fn has_matching_definition(&self, pattern: &Regex) -> bool {
197 self.iter().any(|v| pattern.is_match(v))
198 }
199}
200impl<T: Borrow<str> + Ord> Environment for BTreeSet<T> {
201 #[inline]
202 fn is_defined(&self, var: &str) -> bool {
203 self.contains(var)
204 }
205 #[inline]
206 fn has_matching_definition(&self, pattern: &Regex) -> bool {
207 self.iter().any(|v| pattern.is_match(v.borrow()))
208 }
209}
210
211struct BooleanExprParser<'a> {
213 tokenizer: Tokenizer<'a>,
214 current: Span<Token<'a>>,
215 expr: BooleanExpr,
217}
218impl<'a> BooleanExprParser<'a> {
219 pub fn new(input: &'a str) -> Self {
220 Self {
221 tokenizer: Tokenizer::new(input),
222 current: Span::new(SourceSpan::from(input.as_bytes().len()), Token::Eof),
223 expr: BooleanExpr::default(),
224 }
225 }
226
227 pub fn parse(mut self) -> Result<BooleanExpr, InvalidBooleanExprError> {
247 self.next()?;
248 self.parse_or_expr()?;
249 self.expect(Token::Eof)?;
250
251 Ok(self.expr)
252 }
253
254 fn parse_or_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
255 self.parse_and_expr()?;
256 while self.accept(Token::Or)? {
257 let left = core::mem::take(&mut self.expr);
258 self.parse_and_expr()?;
259 let right = core::mem::take(&mut self.expr);
260 self.expr = BooleanExpr::Or(Box::new(left), Box::new(right));
261 }
262
263 Ok(())
264 }
265
266 fn parse_and_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
267 self.parse_not_expr()?;
268 while self.accept(Token::And)? {
269 let left = core::mem::take(&mut self.expr);
270 self.parse_not_expr()?;
271 let right = core::mem::take(&mut self.expr);
272 self.expr = BooleanExpr::And(Box::new(left), Box::new(right));
273 }
274
275 Ok(())
276 }
277
278 fn parse_not_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
279 match &*self.current {
280 Token::Not => {
281 self.next()?;
282 self.parse_not_expr()?;
283 let value = core::mem::take(&mut self.expr);
284 self.expr = BooleanExpr::Not(Box::new(value));
285 Ok(())
286 }
287 Token::Lparen => {
288 self.next()?;
289 self.parse_or_expr()?;
290 self.expect(Token::Rparen)
291 }
292 Token::Ident(_) | Token::Pattern(_) => self.parse_match_expr(),
293 Token::Eof => Err(InvalidBooleanExprError::UnexpectedEof {
294 span: self.current.span(),
295 expected: vec!["'!'", "'('", "'{{'", "identifier"],
296 }),
297 token => Err(InvalidBooleanExprError::UnexpectedToken {
298 span: self.current.span(),
299 token: token.to_string(),
300 expected: vec!["'!'", "'('", "'{{'", "identifier"],
301 }),
302 }
303 }
304
305 fn parse_match_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
306 use smallvec::SmallVec;
307
308 let start = self.current.start();
309 let mut end = self.current.end();
310 let mut is_ident = true;
311 let mut reserve = 0;
312 let mut parts = SmallVec::<[(SourceSpan, Token<'_>); 4]>::default();
313 loop {
314 let current_span = self.current.span();
315 let current_end = self.current.end();
316 match &*self.current {
317 tok @ Token::Pattern(raw) => {
318 end = current_end;
319 is_ident = false;
320 reserve += raw.len() + 2;
321 parts.push((current_span, *tok));
322 self.next()?;
323 }
324 tok @ Token::Ident(raw) => {
325 end = current_end;
326 parts.push((current_span, *tok));
327 reserve += raw.len() + 8;
328 self.next()?;
329 }
330 _ => break,
331 }
332 }
333
334 if parts.is_empty() {
335 return if self.current == Token::Eof {
336 Err(InvalidBooleanExprError::UnexpectedEof {
337 span: self.current.span(),
338 expected: vec!["pattern", "identifier"],
339 })
340 } else {
341 Err(InvalidBooleanExprError::UnexpectedToken {
342 span: self.current.span(),
343 token: self.current.to_string(),
344 expected: vec!["pattern", "identifier"],
345 })
346 };
347 }
348
349 if is_ident {
350 match parts.as_slice() {
351 [(_, Token::Ident(name))] => {
352 self.expr = match *name {
353 "true" => BooleanExpr::Lit(true),
354 "false" => BooleanExpr::Lit(false),
355 name => BooleanExpr::Var(name.to_string()),
356 };
357 }
358 [_, (span, tok), ..] => {
359 return Err(InvalidBooleanExprError::UnexpectedToken {
360 span: *span,
361 token: tok.to_string(),
362 expected: vec!["end of expression"],
363 });
364 }
365 _ => panic!(
366 "expected ident expression to consist of a single part: {:?}",
367 parts
368 ),
369 }
370 } else {
371 let pattern =
372 parts
373 .into_iter()
374 .fold(
375 String::with_capacity(reserve),
376 |mut acc, (_, token)| match token {
377 Token::Pattern(pat) => {
378 acc.push('(');
379 acc.push_str(pat);
380 acc.push(')');
381 acc
382 }
383 Token::Ident(raw) => {
384 regex_syntax::escape_into(raw, &mut acc);
385 acc
386 }
387 _ => unsafe { core::hint::unreachable_unchecked() },
388 },
389 );
390 self.expr = Regex::new(&pattern)
391 .map(BooleanExpr::Pattern)
392 .map_err(|error| InvalidBooleanExprError::InvalidRegex {
393 span: SourceSpan::from(start..end),
394 error,
395 })?;
396 }
397
398 Ok(())
399 }
400
401 fn accept(&mut self, expected: Token<'a>) -> Result<bool, InvalidBooleanExprError> {
402 if self.current == expected {
403 self.next().map(|_| true)
404 } else {
405 Ok(false)
406 }
407 }
408
409 fn expect(&mut self, expected: Token<'a>) -> Result<(), InvalidBooleanExprError> {
410 if self.current == expected {
411 if self.current != Token::Eof {
412 self.next()?;
413 }
414 Ok(())
415 } else if self.current == Token::Eof {
416 Err(InvalidBooleanExprError::UnexpectedEof {
417 span: self.current.span(),
418 expected: vec![expected.label()],
419 })
420 } else {
421 Err(InvalidBooleanExprError::UnexpectedToken {
422 span: self.current.span(),
423 token: self.current.to_string(),
424 expected: vec![expected.label()],
425 })
426 }
427 }
428
429 #[inline(always)]
430 fn next(&mut self) -> Result<(), InvalidBooleanExprError> {
431 let token = self.tokenizer.next().unwrap_or_else(|| {
432 let span = SourceSpan::from(self.current.end());
433 Ok(Span::new(span, Token::Eof))
434 })?;
435 self.current = token;
436 Ok(())
437 }
438}
439
440#[derive(Debug, Copy, Clone, PartialEq, Eq)]
442enum Token<'a> {
443 Ident(&'a str),
444 Pattern(&'a str),
445 And,
446 Or,
447 Not,
448 Lparen,
449 Rparen,
450 Eof,
451}
452impl<'a> Token<'a> {
453 pub fn label(&self) -> &'static str {
454 match self {
455 Self::Ident(_) => "identifier",
456 Self::Pattern(_) => "pattern",
457 Self::And => "'&&'",
458 Self::Or => "'||'",
459 Self::Not => "'!'",
460 Self::Lparen => "'('",
461 Self::Rparen => "')'",
462 Self::Eof => "end of expression",
463 }
464 }
465}
466impl<'a> fmt::Display for Token<'a> {
467 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
468 match self {
469 Self::Ident(ident) => write!(f, "'{ident}'"),
470 Self::Pattern(pattern) => write!(f, "'{{{{{pattern}}}}}'"),
471 Self::And => f.write_str("'&&'"),
472 Self::Or => f.write_str("'||'"),
473 Self::Not => f.write_str("'!'"),
474 Self::Lparen => f.write_str("'('"),
475 Self::Rparen => f.write_str("')'"),
476 Self::Eof => f.write_str("end of expression"),
477 }
478 }
479}
480
481struct Tokenizer<'a> {
483 input: &'a str,
484 chars: std::iter::Peekable<std::str::Chars<'a>>,
485 token_start: usize,
486 token_end: usize,
487 token: Token<'a>,
488 error: Option<InvalidBooleanExprError>,
489 eof: bool,
490 current: char,
491 pos: usize,
492}
493
494macro_rules! pop {
495 ($tokenizer:ident, $tok:expr) => {{
496 $tokenizer.pop();
497 Ok($tok)
498 }};
499}
500
501macro_rules! pop2 {
502 ($tokenizer:ident, $tok:expr) => {{
503 $tokenizer.pop();
504 $tokenizer.pop();
505 Ok($tok)
506 }};
507}
508
509impl<'a> Tokenizer<'a> {
510 pub fn new(input: &'a str) -> Self {
511 let mut chars = input.chars().peekable();
512 let current = chars.next();
513 let end = current.map(|c| c.len_utf8()).unwrap_or(0);
514 let pos = 0;
515 let current = current.unwrap_or('\0');
516 let mut tokenizer = Self {
517 input,
518 chars,
519 token_start: 0,
520 token_end: end,
521 token: Token::Eof,
522 error: None,
523 eof: false,
524 current,
525 pos,
526 };
527 tokenizer.advance();
528 tokenizer
529 }
530
531 fn lex(&mut self) -> Option<Result<Span<Token<'a>>, InvalidBooleanExprError>> {
532 if self.error.is_some() {
533 return self.lex_error();
534 }
535
536 if self.eof && matches!(self.token, Token::Eof) {
537 return None;
538 }
539
540 let token = core::mem::replace(&mut self.token, Token::Eof);
541 let span = self.span();
542 self.advance();
543
544 Some(Ok(Span::new(span, token)))
545 }
546
547 #[cold]
548 fn lex_error(&mut self) -> Option<Result<Span<Token<'a>>, InvalidBooleanExprError>> {
549 self.eof = true;
550 self.token = Token::Eof;
551 self.error.take().map(Err)
552 }
553
554 fn advance(&mut self) {
555 let (pos, c) = self.read();
556
557 if c == '\0' {
558 self.eof = true;
559 return;
560 }
561
562 self.token_start = pos;
563 match self.tokenize() {
564 Ok(Token::Eof) => {
565 self.token = Token::Eof;
566 self.eof = true;
567 }
568 Ok(token) => {
569 self.token = token;
570 }
571 Err(err) => {
572 self.error = Some(err);
573 }
574 }
575 }
576
577 fn pop(&mut self) -> char {
578 let c = self.current;
579 self.pos += c.len_utf8();
580 self.token_end = self.pos;
581 match self.chars.next() {
582 None => {
583 self.eof = true;
584 self.current = '\0';
585 c
586 }
587 Some(next) => {
588 self.current = next;
589 c
590 }
591 }
592 }
593
594 fn drop(&mut self) {
595 self.pos += self.current.len_utf8();
596 self.token_end = self.pos;
597 self.token_start = self.token_end;
598 match self.chars.next() {
599 None => {
600 self.eof = true;
601 self.current = '\0';
602 }
603 Some(next) => {
604 self.current = next;
605 }
606 }
607 }
608
609 #[inline]
610 fn skip(&mut self) {
611 self.pop();
612 }
613
614 fn read(&self) -> (usize, char) {
615 (self.pos, self.current)
616 }
617
618 fn peek(&mut self) -> char {
619 self.chars.peek().copied().unwrap_or('\0')
620 }
621
622 fn span(&self) -> SourceSpan {
623 SourceSpan::from(self.token_start..self.token_end)
624 }
625
626 fn slice(&self) -> &'a str {
627 unsafe {
628 core::str::from_utf8_unchecked(&self.input.as_bytes()[self.token_start..self.token_end])
629 }
630 }
631
632 fn span_slice(&self, span: impl Into<Range<usize>>) -> &'a str {
633 let span = span.into();
634 unsafe { core::str::from_utf8_unchecked(&self.input.as_bytes()[span.start..span.end]) }
635 }
636
637 fn tokenize(&mut self) -> Result<Token<'a>, InvalidBooleanExprError> {
638 self.drop_while(char::is_whitespace);
639
640 let (pos, c) = self.read();
641 match c {
642 '(' => pop!(self, Token::Lparen),
643 ')' => pop!(self, Token::Rparen),
644 '&' => match self.peek() {
645 '&' => pop2!(self, Token::And),
646 '\0' => Err(InvalidBooleanExprError::UnexpectedEof {
647 span: SourceSpan::from(pos..self.token_end),
648 expected: vec!["'&'"],
649 }),
650 c => Err(InvalidBooleanExprError::UnexpectedChar {
651 span: SourceSpan::from(pos..(self.token_end + c.len_utf8())),
652 c,
653 }),
654 },
655 '|' => match self.peek() {
656 '|' => pop2!(self, Token::Or),
657 '\0' => Err(InvalidBooleanExprError::UnexpectedEof {
658 span: SourceSpan::from(pos..self.token_end),
659 expected: vec!["'|'"],
660 }),
661 c => Err(InvalidBooleanExprError::UnexpectedChar {
662 span: SourceSpan::from(pos..(self.token_end + c.len_utf8())),
663 c,
664 }),
665 },
666 '!' => pop!(self, Token::Not),
667 '-' | '+' | '=' | '.' | '_' | 'a'..='z' | 'A'..='Z' | '0'..='9' => {
668 self.skip_while(|c| matches!(c, '-' | '+' | '=' | '.' | '_' | 'a'..='z' | 'A'..='Z' | '0'..='9'));
669 Ok(Token::Ident(self.slice()))
670 }
671 '{' => {
672 if self.peek() == '{' {
673 self.skip();
674 self.skip();
675 }
676 let start = self.pos;
677 self.skip_while(|c| c != '}');
678 let end = self.pos;
679 let next = self.peek();
680 match self.read() {
681 (_, '}') if next == '}' => {
683 self.pop();
684 self.pop();
685 let pattern = self.span_slice(start..end);
686 if pattern.is_empty() {
687 Err(InvalidBooleanExprError::UnexpectedToken {
688 span: SourceSpan::from(start..end),
689 token: "'}}'".to_string(),
690 expected: vec!["pattern"],
691 })
692 } else {
693 Ok(Token::Pattern(pattern))
694 }
695 }
696 (_, '}') if next != '\0' => Err(InvalidBooleanExprError::UnexpectedChar {
697 span: SourceSpan::from(end),
698 c: next,
699 }),
700 (_, _) => Err(InvalidBooleanExprError::UnexpectedEof {
701 span: SourceSpan::from(end),
702 expected: vec!["'}'"],
703 }),
704 }
705 }
706 '\0' => Ok(Token::Eof),
707 c => Err(InvalidBooleanExprError::UnexpectedChar {
708 span: SourceSpan::from(pos),
709 c,
710 }),
711 }
712 }
713
714 #[inline]
715 fn drop_while<F>(&mut self, predicate: F)
716 where
717 F: Fn(char) -> bool,
718 {
719 loop {
720 match self.read() {
721 (_, '\0') => break,
722 (_, c) if predicate(c) => self.drop(),
723 _ => break,
724 }
725 }
726 }
727
728 #[inline]
729 fn skip_while<F>(&mut self, predicate: F)
730 where
731 F: Fn(char) -> bool,
732 {
733 loop {
734 match self.read() {
735 (_, '\0') => break,
736 (_, c) if predicate(c) => self.skip(),
737 _ => break,
738 }
739 }
740 }
741}
742impl<'a> Iterator for Tokenizer<'a> {
743 type Item = Result<Span<Token<'a>>, InvalidBooleanExprError>;
744
745 #[inline]
746 fn next(&mut self) -> Option<Self::Item> {
747 self.lex()
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use std::fmt::Write;
754
755 use super::*;
756 use litcheck::diagnostics::Report;
757
758 use pretty_assertions::assert_eq;
759
760 macro_rules! parse {
761 ($input:literal, $expected:expr) => {
762 match $input.parse::<BooleanExpr>() {
763 Ok(expr) => {
764 assert_eq!(expr, $expected);
765 expr
766 }
767 Err(err) => panic!("failed to parse boolean expression: {err}"),
768 }
769 };
770 }
771
772 macro_rules! assert_parse_error {
773 ($input:literal) => {
774 match $input
775 .parse::<BooleanExpr>()
776 .map_err(|err| Report::new(err).with_source_code($input))
777 {
778 Err(err) => {
779 let mut msg = format!("{}", &err);
780 if let Some(labels) = err.labels() {
781 let mut n = 0;
782 for label in labels {
783 if let Some(label) = label.label() {
784 if n > 0 {
785 msg.push_str(", ");
786 } else {
787 msg.push_str(": ");
788 }
789 msg.push_str(label);
790 n += 1;
791 }
792 }
793 }
794 if let Some(help) = err.help() {
795 write!(&mut msg, "\nhelp: {help}").unwrap();
796 }
797 panic!("{msg}");
798 }
799 _ => (),
800 }
801 };
802 }
803
804 macro_rules! assert_eval_true {
805 ($expr:ident, $vars:ident) => {{
806 assert_eval(&$expr, &$vars, true);
807 }};
808
809 ($expr:ident, $vars:ident, $($var:literal),+) => {
810 $vars.clear();
811 $vars.extend([$($var),*]);
812 assert_eval(&$expr, &$vars, true);
813 }
814 }
815
816 macro_rules! assert_eval_false {
817 ($expr:ident, $vars:ident) => {{
818 assert_eval(&$expr, &$vars, false);
819 }};
820
821 ($expr:ident, $vars:ident, $($var:literal),+) => {{
822 $vars.clear();
823 $vars.extend([$($var),*]);
824 assert_eval(&$expr, &$vars, false);
825 }}
826 }
827
828 macro_rules! and {
829 ($lhs:expr, $rhs:expr) => {
830 BooleanExpr::And(Box::new($lhs), Box::new($rhs))
831 };
832 }
833
834 macro_rules! or {
835 ($lhs:expr, $rhs:expr) => {
836 BooleanExpr::Or(Box::new($lhs), Box::new($rhs))
837 };
838 }
839
840 macro_rules! not {
841 ($lhs:expr) => {
842 BooleanExpr::Not(Box::new($lhs))
843 };
844 }
845
846 macro_rules! var {
847 ($name:literal) => {
848 BooleanExpr::Var($name.to_string())
849 };
850 }
851
852 macro_rules! pattern {
853 ($regex:literal) => {
854 BooleanExpr::Pattern(Regex::new($regex).unwrap())
855 };
856 }
857
858 #[test]
859 fn test_script_boolean_expr_variables() {
860 let vars = BTreeSet::from([
861 "its-true",
862 "false-lol-true",
863 "under_score",
864 "e=quals",
865 "d1g1ts",
866 ]);
867
868 let expr = parse!("true", BooleanExpr::Lit(true));
869 assert_eval_true!(expr, vars);
870 let expr = parse!("false", BooleanExpr::Lit(false));
871 assert_eval_false!(expr, vars);
872 let expr = parse!("its-true", var!("its-true"));
873 assert_eval_true!(expr, vars);
874 let expr = parse!("under_score", var!("under_score"));
875 assert_eval_true!(expr, vars);
876 let expr = parse!("e=quals", var!("e=quals"));
877 assert_eval_true!(expr, vars);
878 let expr = parse!("d1g1ts", var!("d1g1ts"));
879 assert_eval_true!(expr, vars);
880 let expr = parse!("{{its.+}}", pattern!("(its.+)"));
881 assert_eval_true!(expr, vars);
882 let expr = parse!("{{false-[lo]+-true}}", pattern!("(false-[lo]+-true)"));
883 assert_eval_true!(expr, vars);
884 let expr = parse!(
885 "{{(true|false)-lol-(true|false)}}",
886 pattern!("((true|false)-lol-(true|false))")
887 );
888 assert_eval_true!(expr, vars);
889 let expr = parse!("d1g{{[0-9]}}ts", pattern!("d1g([0-9])ts"));
890 assert_eval_true!(expr, vars);
891 let expr = parse!("d1g{{[0-9]}}t{{[a-z]}}", pattern!("d1g([0-9])t([a-z])"));
892 assert_eval_true!(expr, vars);
893 let expr = parse!("d1{{(g|1)+}}ts", pattern!("d1((g|1)+)ts"));
894 assert_eval_true!(expr, vars);
895 }
896
897 #[test]
898 fn test_script_boolean_expr_operators_or() {
899 let vars = BTreeSet::default();
900
901 let expr = parse!(
902 "true || true",
903 or!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
904 );
905 assert_eval_true!(expr, vars);
906 let expr = parse!(
907 "true || false",
908 or!(BooleanExpr::Lit(true), BooleanExpr::Lit(false))
909 );
910 assert_eval_true!(expr, vars);
911 let expr = parse!(
912 "false || true",
913 or!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
914 );
915 assert_eval_true!(expr, vars);
916 let expr = parse!(
917 "false || false",
918 or!(BooleanExpr::Lit(false), BooleanExpr::Lit(false))
919 );
920 assert_eval_false!(expr, vars);
921 }
922
923 #[test]
924 fn test_script_boolean_expr_operators_and() {
925 let vars = BTreeSet::default();
926
927 let expr = parse!(
928 "true && true",
929 and!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
930 );
931 assert_eval_true!(expr, vars);
932 let expr = parse!(
933 "true && false",
934 and!(BooleanExpr::Lit(true), BooleanExpr::Lit(false))
935 );
936 assert_eval_false!(expr, vars);
937 let expr = parse!(
938 "false && true",
939 and!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
940 );
941 assert_eval_false!(expr, vars);
942 let expr = parse!(
943 "false && false",
944 and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false))
945 );
946 assert_eval_false!(expr, vars);
947 }
948
949 #[test]
950 fn test_script_boolean_expr_operators_not() {
951 let vars = BTreeSet::default();
952
953 let expr = parse!("!true", not!(BooleanExpr::Lit(true)));
954 assert_eval_false!(expr, vars);
955 let expr = parse!("!false", not!(BooleanExpr::Lit(false)));
956 assert_eval_true!(expr, vars);
957 let expr = parse!("!!false", not!(not!(BooleanExpr::Lit(false))));
958 assert_eval_false!(expr, vars);
959 }
960
961 #[test]
962 fn test_script_boolean_expr_operators_mixed() {
963 let vars = BTreeSet::default();
964
965 let expr = parse!(" ((!((false) )) ) ", not!(BooleanExpr::Lit(false)));
966 assert_eval_true!(expr, vars);
967 let expr = parse!(
968 "true && (true && (true))",
969 and!(
970 BooleanExpr::Lit(true),
971 and!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
972 )
973 );
974 assert_eval_true!(expr, vars);
975 let expr = parse!(
976 "!false && !false && !! !false",
977 and!(
978 and!(not!(BooleanExpr::Lit(false)), not!(BooleanExpr::Lit(false))),
979 not!(not!(not!(BooleanExpr::Lit(false))))
980 )
981 );
982 assert_eval_true!(expr, vars);
983 let expr = parse!(
984 "false && false || true",
985 or!(
986 and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false)),
987 BooleanExpr::Lit(true)
988 )
989 );
990 assert_eval_true!(expr, vars);
991 let expr = parse!(
992 "(false && false) || true",
993 or!(
994 and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false)),
995 BooleanExpr::Lit(true)
996 )
997 );
998 assert_eval_true!(expr, vars);
999 let expr = parse!(
1000 "false && (false || true)",
1001 and!(
1002 BooleanExpr::Lit(false),
1003 or!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
1004 )
1005 );
1006 assert_eval_false!(expr, vars);
1007 }
1008
1009 #[test]
1010 fn test_script_boolean_expr_evaluate() {
1011 let mut vars = BTreeSet::from(["linux", "target=x86_64-unknown-linux-gnu"]);
1012
1013 let expr = parse!(
1014 "linux && (target={{aarch64-.+}} || target={{x86_64-.+}})",
1015 and!(
1016 var!("linux"),
1017 or!(
1018 pattern!("target=(aarch64-.+)"),
1019 pattern!("target=(x86_64-.+)")
1020 )
1021 )
1022 );
1023 assert!(expr.evaluate(&vars));
1024 vars.clear();
1025 vars.extend(["linux", "target=i386-unknown-linux-gnu"]);
1026 assert!(!expr.evaluate(&vars));
1027
1028 let expr = parse!(
1029 "use_system_cxx_lib && target={{.+}}-apple-macosx10.{{9|10|11|12}} && !no-exceptions",
1030 and!(
1031 and!(
1032 var!("use_system_cxx_lib"),
1033 pattern!("target=(.+)\\-apple\\-macosx10\\.(9|10|11|12)")
1034 ),
1035 not!(var!("no-exceptions"))
1036 )
1037 );
1038 vars.clear();
1039 vars.extend(["use_system_cxx_lib", "target=arm64-apple-macosx10.12"]);
1040 assert!(expr.evaluate(&vars));
1041 vars.insert("no-exceptions");
1042 assert!(!expr.evaluate(&vars));
1043 vars.clear();
1044 vars.extend(["use_system_cxx_lib", "target=arm64-apple-macosx10.15"]);
1045 assert!(!expr.evaluate(&vars));
1046 }
1047
1048 #[test]
1049 #[should_panic(expected = "unexpected character: '#' is not valid here")]
1050 fn test_script_boolean_expr_invalid_ident() {
1051 assert_parse_error!("ba#d");
1052 }
1053
1054 #[test]
1055 #[should_panic(
1056 expected = "unexpected token: 'and' is not valid here\nhelp: expected one of: end of expression"
1057 )]
1058 fn test_script_boolean_expr_invalid_operator() {
1059 assert_parse_error!("true and true");
1060 }
1061
1062 #[test]
1063 #[should_panic(
1064 expected = "unexpected token: '||' is not valid here\nhelp: expected one of: '!', '(', '{{', identifier"
1065 )]
1066 fn test_script_boolean_expr_missing_lhs_operand() {
1067 assert_parse_error!("|| true");
1068 }
1069
1070 #[test]
1071 #[should_panic(
1072 expected = "unexpected end of expression: occurs here\nhelp: expected one of: '!', '(', '{{', identifier"
1073 )]
1074 fn test_script_boolean_expr_missing_rhs_operand() {
1075 assert_parse_error!("true &&");
1076 }
1077
1078 #[test]
1079 #[should_panic(
1080 expected = "unexpected end of expression: occurs here\nhelp: expected one of: '!', '(', '{{', identifier"
1081 )]
1082 fn test_script_boolean_expr_empty_input() {
1083 assert_parse_error!("");
1084 }
1085
1086 #[test]
1087 #[should_panic(
1088 expected = "unexpected end of expression: occurs here\nhelp: expected one of: ')'"
1089 )]
1090 fn test_script_boolean_expr_unclosed_group() {
1091 assert_parse_error!("(((true && true) || true)");
1092 }
1093
1094 #[test]
1095 #[should_panic(
1096 expected = "unexpected token: '(' is not valid here\nhelp: expected one of: end of expression"
1097 )]
1098 fn test_script_boolean_expr_ident_followed_by_group() {
1099 assert_parse_error!("true (true)");
1100 }
1101
1102 #[test]
1103 #[should_panic(
1104 expected = "unexpected token: ')' is not valid here\nhelp: expected one of: '!', '(', '{{', identifier"
1105 )]
1106 fn test_script_boolean_expr_empty_group() {
1107 assert_parse_error!("( )");
1108 }
1109
1110 #[test]
1111 #[should_panic(
1112 expected = "unexpected end of expression: occurs here\nhelp: expected one of: '}'"
1113 )]
1114 fn test_script_boolean_expr_unclosed_pattern() {
1115 assert_parse_error!("abc{def");
1116 }
1117
1118 #[test]
1119 #[should_panic(
1120 expected = "unexpected end of expression: occurs here\nhelp: expected one of: '}'"
1121 )]
1122 fn test_script_boolean_expr_empty_pattern() {
1123 assert_parse_error!("{}");
1124 }
1125
1126 #[track_caller]
1127 #[inline(never)]
1128 fn assert_eval(expr: &BooleanExpr, vars: &BTreeSet<&'static str>, expected_result: bool) {
1129 let result = expr.evaluate(vars);
1130 assert_eq!(
1131 result, expected_result,
1132 "expected {expr} to evaluate to {expected_result} with environment: {vars:?}"
1133 );
1134 }
1135}