litcheck_lit/config/expr/
boolean.rs

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/// This error is produced when attempting to parse a [BooleanExpr]
10#[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/// This corresponds to the boolean expressions allowed in lit's `DEFINE` and `REDEFINE` commands
60#[derive(Debug, Clone)]
61pub enum BooleanExpr {
62    /// A literal boolean value
63    Lit(bool),
64    /// A variable name.
65    ///
66    /// If the variable exists, this evaluates the value of that variable, otherwise false.
67    Var(String),
68    /// A pattern which, if it matches any variable which is true, evaluates to true, otherwise false.
69    Pattern(Regex),
70    /// The logical NOT operator
71    Not(Box<BooleanExpr>),
72    /// The logical AND operator
73    And(Box<BooleanExpr>, Box<BooleanExpr>),
74    /// The logical OR operator
75    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    /// Evaluate this boolean expression with `variables` as the context.
150    ///
151    /// If a variable is present in `variables`, it is considered to have an implicit
152    /// value of true. As a result, if an identifier or pattern matches a variable in
153    /// the set, that expression evaluates to true.
154    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
211/// This struct parses a [BooleanExpr] from a given input string
212struct BooleanExprParser<'a> {
213    tokenizer: Tokenizer<'a>,
214    current: Span<Token<'a>>,
215    /// The current expression parsed thus far
216    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    /// This corresponds to the following grammar:
228    ///
229    /// ```text,ignore
230    /// expr        ::= or_expr
231    /// or_expr     ::= and_expr ('||' and_expr)*
232    /// and_expr    ::= not_expr ('&&' not_expr)*
233    /// not_expr    ::= '!' not_expr
234    ///               | '(' or_expr ')'
235    ///               | match_expr
236    /// match_expr  ::= pattern
237    ///               | var
238    /// var         ::= identifier
239    /// pattern     ::= regex pattern
240    ///               | identifier pattern
241    ///               | regex
242    ///               | identifier
243    /// identifier  ::= [-+=._a-zA-Z0-9]+
244    /// regex       ::= '{{' <any valid regular expression> '}}'
245    /// ```
246    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/// The token type produced by [Tokenizer]
441#[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
481/// The tokenizer for [BooleanExprParser]
482struct 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                    // Can only happen if there is nothing in the braces
682                    (_, '}') 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}