litcheck_lit/config/expr/
boolean.rs

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