Skip to main content

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, SourceId, SourceSpan, Span};
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().to_usize() + offset;
53                let end = span.end().to_usize() + offset;
54                *span = SourceSpan::from_range_unchecked(span.source_id(), 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(SourceId::UNKNOWN, 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(source_id: SourceId, input: &'a str) -> Self {
222        Self {
223            tokenizer: Tokenizer::new(source_id, input),
224            current: Span::new(
225                SourceSpan::from_range_unchecked(source_id, 0..input.len()),
226                Token::Eof,
227            ),
228            expr: BooleanExpr::default(),
229        }
230    }
231
232    /// This corresponds to the following grammar:
233    ///
234    /// ```text,ignore
235    /// expr        ::= or_expr
236    /// or_expr     ::= and_expr ('||' and_expr)*
237    /// and_expr    ::= not_expr ('&&' not_expr)*
238    /// not_expr    ::= '!' not_expr
239    ///               | '(' or_expr ')'
240    ///               | match_expr
241    /// match_expr  ::= pattern
242    ///               | var
243    /// var         ::= identifier
244    /// pattern     ::= regex pattern
245    ///               | identifier pattern
246    ///               | regex
247    ///               | identifier
248    /// identifier  ::= [-+=._a-zA-Z0-9]+
249    /// regex       ::= '{{' <any valid regular expression> '}}'
250    /// ```
251    pub fn parse(mut self) -> Result<BooleanExpr, InvalidBooleanExprError> {
252        self.next()?;
253        self.parse_or_expr()?;
254        self.expect(Token::Eof)?;
255
256        Ok(self.expr)
257    }
258
259    fn parse_or_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
260        self.parse_and_expr()?;
261        while self.accept(Token::Or)? {
262            let left = core::mem::take(&mut self.expr);
263            self.parse_and_expr()?;
264            let right = core::mem::take(&mut self.expr);
265            self.expr = BooleanExpr::Or(Box::new(left), Box::new(right));
266        }
267
268        Ok(())
269    }
270
271    fn parse_and_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
272        self.parse_not_expr()?;
273        while self.accept(Token::And)? {
274            let left = core::mem::take(&mut self.expr);
275            self.parse_not_expr()?;
276            let right = core::mem::take(&mut self.expr);
277            self.expr = BooleanExpr::And(Box::new(left), Box::new(right));
278        }
279
280        Ok(())
281    }
282
283    fn parse_not_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
284        match &*self.current {
285            Token::Not => {
286                self.next()?;
287                self.parse_not_expr()?;
288                let value = core::mem::take(&mut self.expr);
289                self.expr = BooleanExpr::Not(Box::new(value));
290                Ok(())
291            }
292            Token::Lparen => {
293                self.next()?;
294                self.parse_or_expr()?;
295                self.expect(Token::Rparen)
296            }
297            Token::Ident(_) | Token::Pattern(_) => self.parse_match_expr(),
298            Token::Eof => Err(InvalidBooleanExprError::UnexpectedEof {
299                span: self.current.span(),
300                expected: vec!["'!'", "'('", "'{{'", "identifier"],
301            }),
302            token => Err(InvalidBooleanExprError::UnexpectedToken {
303                span: self.current.span(),
304                token: token.to_string(),
305                expected: vec!["'!'", "'('", "'{{'", "identifier"],
306            }),
307        }
308    }
309
310    fn parse_match_expr(&mut self) -> Result<(), InvalidBooleanExprError> {
311        use smallvec::SmallVec;
312
313        let start = self.current.span().start().to_usize();
314        let mut end = self.current.span().end().to_usize();
315        let mut is_ident = true;
316        let mut reserve = 0;
317        let mut parts = SmallVec::<[(SourceSpan, Token<'_>); 4]>::default();
318        loop {
319            let current_span = self.current.span();
320            let current_end = self.current.span().end().to_usize();
321            match &*self.current {
322                tok @ Token::Pattern(raw) => {
323                    end = current_end;
324                    is_ident = false;
325                    reserve += raw.len() + 2;
326                    parts.push((current_span, *tok));
327                    self.next()?;
328                }
329                tok @ Token::Ident(raw) => {
330                    end = current_end;
331                    parts.push((current_span, *tok));
332                    reserve += raw.len() + 8;
333                    self.next()?;
334                }
335                _ => break,
336            }
337        }
338
339        if parts.is_empty() {
340            return if self.current == Token::Eof {
341                Err(InvalidBooleanExprError::UnexpectedEof {
342                    span: self.current.span(),
343                    expected: vec!["pattern", "identifier"],
344                })
345            } else {
346                Err(InvalidBooleanExprError::UnexpectedToken {
347                    span: self.current.span(),
348                    token: self.current.to_string(),
349                    expected: vec!["pattern", "identifier"],
350                })
351            };
352        }
353
354        if is_ident {
355            match parts.as_slice() {
356                [(_, Token::Ident(name))] => {
357                    self.expr = match *name {
358                        "true" => BooleanExpr::Lit(true),
359                        "false" => BooleanExpr::Lit(false),
360                        name => BooleanExpr::Var(name.to_string()),
361                    };
362                }
363                [_, (span, tok), ..] => {
364                    return Err(InvalidBooleanExprError::UnexpectedToken {
365                        span: *span,
366                        token: tok.to_string(),
367                        expected: vec!["end of expression"],
368                    });
369                }
370                _ => panic!(
371                    "expected ident expression to consist of a single part: {:?}",
372                    parts
373                ),
374            }
375        } else {
376            let pattern =
377                parts
378                    .into_iter()
379                    .fold(
380                        String::with_capacity(reserve),
381                        |mut acc, (_, token)| match token {
382                            Token::Pattern(pat) => {
383                                acc.push('(');
384                                acc.push_str(pat);
385                                acc.push(')');
386                                acc
387                            }
388                            Token::Ident(raw) => {
389                                regex_syntax::escape_into(raw, &mut acc);
390                                acc
391                            }
392                            _ => unsafe { core::hint::unreachable_unchecked() },
393                        },
394                    );
395            self.expr = Regex::new(&pattern)
396                .map(BooleanExpr::Pattern)
397                .map_err(|error| InvalidBooleanExprError::InvalidRegex {
398                    span: SourceSpan::from_range_unchecked(
399                        self.current.span().source_id(),
400                        start..end,
401                    ),
402                    error,
403                })?;
404        }
405
406        Ok(())
407    }
408
409    fn accept(&mut self, expected: Token<'a>) -> Result<bool, InvalidBooleanExprError> {
410        if self.current == expected {
411            self.next().map(|_| true)
412        } else {
413            Ok(false)
414        }
415    }
416
417    fn expect(&mut self, expected: Token<'a>) -> Result<(), InvalidBooleanExprError> {
418        if self.current == expected {
419            if self.current != Token::Eof {
420                self.next()?;
421            }
422            Ok(())
423        } else if self.current == Token::Eof {
424            Err(InvalidBooleanExprError::UnexpectedEof {
425                span: self.current.span(),
426                expected: vec![expected.label()],
427            })
428        } else {
429            Err(InvalidBooleanExprError::UnexpectedToken {
430                span: self.current.span(),
431                token: self.current.to_string(),
432                expected: vec![expected.label()],
433            })
434        }
435    }
436
437    #[inline(always)]
438    fn next(&mut self) -> Result<(), InvalidBooleanExprError> {
439        let token = self.tokenizer.next().unwrap_or_else(|| {
440            let span = SourceSpan::at(self.current.span().source_id(), self.current.span().end());
441            Ok(Span::new(span, Token::Eof))
442        })?;
443        self.current = token;
444        Ok(())
445    }
446}
447
448/// The token type produced by [Tokenizer]
449#[derive(Debug, Copy, Clone, PartialEq, Eq)]
450enum Token<'a> {
451    Ident(&'a str),
452    Pattern(&'a str),
453    And,
454    Or,
455    Not,
456    Lparen,
457    Rparen,
458    Eof,
459}
460impl<'a> Token<'a> {
461    pub fn label(&self) -> &'static str {
462        match self {
463            Self::Ident(_) => "identifier",
464            Self::Pattern(_) => "pattern",
465            Self::And => "'&&'",
466            Self::Or => "'||'",
467            Self::Not => "'!'",
468            Self::Lparen => "'('",
469            Self::Rparen => "')'",
470            Self::Eof => "end of expression",
471        }
472    }
473}
474impl<'a> fmt::Display for Token<'a> {
475    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
476        match self {
477            Self::Ident(ident) => write!(f, "'{ident}'"),
478            Self::Pattern(pattern) => write!(f, "'{{{{{pattern}}}}}'"),
479            Self::And => f.write_str("'&&'"),
480            Self::Or => f.write_str("'||'"),
481            Self::Not => f.write_str("'!'"),
482            Self::Lparen => f.write_str("'('"),
483            Self::Rparen => f.write_str("')'"),
484            Self::Eof => f.write_str("end of expression"),
485        }
486    }
487}
488
489/// The tokenizer for [BooleanExprParser]
490struct Tokenizer<'a> {
491    source_id: SourceId,
492    input: &'a str,
493    chars: std::iter::Peekable<std::str::Chars<'a>>,
494    token_start: usize,
495    token_end: usize,
496    token: Token<'a>,
497    error: Option<InvalidBooleanExprError>,
498    eof: bool,
499    current: char,
500    pos: usize,
501}
502
503macro_rules! pop {
504    ($tokenizer:ident, $tok:expr) => {{
505        $tokenizer.pop();
506        Ok($tok)
507    }};
508}
509
510macro_rules! pop2 {
511    ($tokenizer:ident, $tok:expr) => {{
512        $tokenizer.pop();
513        $tokenizer.pop();
514        Ok($tok)
515    }};
516}
517
518impl<'a> Tokenizer<'a> {
519    pub fn new(source_id: SourceId, input: &'a str) -> Self {
520        let mut chars = input.chars().peekable();
521        let current = chars.next();
522        let end = current.map(|c| c.len_utf8()).unwrap_or(0);
523        let pos = 0;
524        let current = current.unwrap_or('\0');
525        let mut tokenizer = Self {
526            source_id,
527            input,
528            chars,
529            token_start: 0,
530            token_end: end,
531            token: Token::Eof,
532            error: None,
533            eof: false,
534            current,
535            pos,
536        };
537        tokenizer.advance();
538        tokenizer
539    }
540
541    fn lex(&mut self) -> Option<Result<Span<Token<'a>>, InvalidBooleanExprError>> {
542        if self.error.is_some() {
543            return self.lex_error();
544        }
545
546        if self.eof && matches!(self.token, Token::Eof) {
547            return None;
548        }
549
550        let token = core::mem::replace(&mut self.token, Token::Eof);
551        let span = self.span();
552        self.advance();
553
554        Some(Ok(Span::new(span, token)))
555    }
556
557    #[cold]
558    fn lex_error(&mut self) -> Option<Result<Span<Token<'a>>, InvalidBooleanExprError>> {
559        self.eof = true;
560        self.token = Token::Eof;
561        self.error.take().map(Err)
562    }
563
564    fn advance(&mut self) {
565        let (pos, c) = self.read();
566
567        if c == '\0' {
568            self.eof = true;
569            return;
570        }
571
572        self.token_start = pos;
573        match self.tokenize() {
574            Ok(Token::Eof) => {
575                self.token = Token::Eof;
576                self.eof = true;
577            }
578            Ok(token) => {
579                self.token = token;
580            }
581            Err(err) => {
582                self.error = Some(err);
583            }
584        }
585    }
586
587    fn pop(&mut self) -> char {
588        let c = self.current;
589        self.pos += c.len_utf8();
590        self.token_end = self.pos;
591        match self.chars.next() {
592            None => {
593                self.eof = true;
594                self.current = '\0';
595                c
596            }
597            Some(next) => {
598                self.current = next;
599                c
600            }
601        }
602    }
603
604    fn drop(&mut self) {
605        self.pos += self.current.len_utf8();
606        self.token_end = self.pos;
607        self.token_start = self.token_end;
608        match self.chars.next() {
609            None => {
610                self.eof = true;
611                self.current = '\0';
612            }
613            Some(next) => {
614                self.current = next;
615            }
616        }
617    }
618
619    #[inline]
620    fn skip(&mut self) {
621        self.pop();
622    }
623
624    fn read(&self) -> (usize, char) {
625        (self.pos, self.current)
626    }
627
628    fn peek(&mut self) -> char {
629        self.chars.peek().copied().unwrap_or('\0')
630    }
631
632    fn span(&self) -> SourceSpan {
633        SourceSpan::from_range_unchecked(self.source_id, self.token_start..self.token_end)
634    }
635
636    fn slice(&self) -> &'a str {
637        unsafe {
638            core::str::from_utf8_unchecked(&self.input.as_bytes()[self.token_start..self.token_end])
639        }
640    }
641
642    fn span_slice(&self, span: impl Into<Range<usize>>) -> &'a str {
643        let span = span.into();
644        unsafe { core::str::from_utf8_unchecked(&self.input.as_bytes()[span.start..span.end]) }
645    }
646
647    fn tokenize(&mut self) -> Result<Token<'a>, InvalidBooleanExprError> {
648        self.drop_while(char::is_whitespace);
649
650        let (pos, c) = self.read();
651        match c {
652            '(' => pop!(self, Token::Lparen),
653            ')' => pop!(self, Token::Rparen),
654            '&' => match self.peek() {
655                '&' => pop2!(self, Token::And),
656                '\0' => Err(InvalidBooleanExprError::UnexpectedEof {
657                    span: SourceSpan::from_range_unchecked(self.source_id, pos..self.token_end),
658                    expected: vec!["'&'"],
659                }),
660                c => Err(InvalidBooleanExprError::UnexpectedChar {
661                    span: SourceSpan::from_range_unchecked(
662                        self.source_id,
663                        pos..(self.token_end + c.len_utf8()),
664                    ),
665                    c,
666                }),
667            },
668            '|' => match self.peek() {
669                '|' => pop2!(self, Token::Or),
670                '\0' => Err(InvalidBooleanExprError::UnexpectedEof {
671                    span: SourceSpan::from_range_unchecked(self.source_id, pos..self.token_end),
672                    expected: vec!["'|'"],
673                }),
674                c => Err(InvalidBooleanExprError::UnexpectedChar {
675                    span: SourceSpan::from_range_unchecked(
676                        self.source_id,
677                        pos..(self.token_end + c.len_utf8()),
678                    ),
679                    c,
680                }),
681            },
682            '!' => pop!(self, Token::Not),
683            '-' | '+' | '=' | '.' | '_' | 'a'..='z' | 'A'..='Z' | '0'..='9' => {
684                self.skip_while(|c| matches!(c, '-' | '+' | '=' | '.' | '_' | 'a'..='z' | 'A'..='Z' | '0'..='9'));
685                Ok(Token::Ident(self.slice()))
686            }
687            '{' => {
688                if self.peek() == '{' {
689                    self.skip();
690                    self.skip();
691                }
692                let start = self.pos;
693                self.skip_while(|c| c != '}');
694                let end = self.pos;
695                let next = self.peek();
696                match self.read() {
697                    // Can only happen if there is nothing in the braces
698                    (_, '}') if next == '}' => {
699                        self.pop();
700                        self.pop();
701                        let pattern = self.span_slice(start..end);
702                        if pattern.is_empty() {
703                            Err(InvalidBooleanExprError::UnexpectedToken {
704                                span: SourceSpan::from_range_unchecked(self.source_id, start..end),
705                                token: "'}}'".to_string(),
706                                expected: vec!["pattern"],
707                            })
708                        } else {
709                            Ok(Token::Pattern(pattern))
710                        }
711                    }
712                    (_, '}') if next != '\0' => Err(InvalidBooleanExprError::UnexpectedChar {
713                        span: SourceSpan::at(self.source_id, end as u32),
714                        c: next,
715                    }),
716                    (_, _) => Err(InvalidBooleanExprError::UnexpectedEof {
717                        span: SourceSpan::at(self.source_id, end as u32),
718                        expected: vec!["'}'"],
719                    }),
720                }
721            }
722            '\0' => Ok(Token::Eof),
723            c => Err(InvalidBooleanExprError::UnexpectedChar {
724                span: SourceSpan::at(self.source_id, pos as u32),
725                c,
726            }),
727        }
728    }
729
730    #[inline]
731    fn drop_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.drop(),
739                _ => break,
740            }
741        }
742    }
743
744    #[inline]
745    fn skip_while<F>(&mut self, predicate: F)
746    where
747        F: Fn(char) -> bool,
748    {
749        loop {
750            match self.read() {
751                (_, '\0') => break,
752                (_, c) if predicate(c) => self.skip(),
753                _ => break,
754            }
755        }
756    }
757}
758impl<'a> Iterator for Tokenizer<'a> {
759    type Item = Result<Span<Token<'a>>, InvalidBooleanExprError>;
760
761    #[inline]
762    fn next(&mut self) -> Option<Self::Item> {
763        self.lex()
764    }
765}
766
767#[cfg(test)]
768mod tests {
769    use std::fmt::Write;
770
771    use super::*;
772    use litcheck::diagnostics::Report;
773
774    use pretty_assertions::assert_eq;
775
776    macro_rules! parse {
777        ($input:literal, $expected:expr) => {
778            match $input.parse::<BooleanExpr>() {
779                Ok(expr) => {
780                    assert_eq!(expr, $expected);
781                    expr
782                }
783                Err(err) => panic!("failed to parse boolean expression: {err}"),
784            }
785        };
786    }
787
788    macro_rules! assert_parse_error {
789        ($input:literal) => {
790            match $input
791                .parse::<BooleanExpr>()
792                .map_err(|err| Report::new(err).with_source_code($input))
793            {
794                Err(err) => {
795                    let mut msg = format!("{}", &err);
796                    if let Some(labels) = err.labels() {
797                        let mut n = 0;
798                        for label in labels {
799                            if let Some(label) = label.label() {
800                                if n > 0 {
801                                    msg.push_str(", ");
802                                } else {
803                                    msg.push_str(": ");
804                                }
805                                msg.push_str(label);
806                                n += 1;
807                            }
808                        }
809                    }
810                    if let Some(help) = err.help() {
811                        write!(&mut msg, "\nhelp: {help}").unwrap();
812                    }
813                    panic!("{msg}");
814                }
815                _ => (),
816            }
817        };
818    }
819
820    macro_rules! assert_eval_true {
821        ($expr:ident, $vars:ident) => {{
822            assert_eval(&$expr, &$vars, true);
823        }};
824
825        ($expr:ident, $vars:ident, $($var:literal),+) => {
826            $vars.clear();
827            $vars.extend([$($var),*]);
828            assert_eval(&$expr, &$vars, true);
829        }
830    }
831
832    macro_rules! assert_eval_false {
833        ($expr:ident, $vars:ident) => {{
834            assert_eval(&$expr, &$vars, false);
835        }};
836
837        ($expr:ident, $vars:ident, $($var:literal),+) => {{
838            $vars.clear();
839            $vars.extend([$($var),*]);
840            assert_eval(&$expr, &$vars, false);
841        }}
842    }
843
844    macro_rules! and {
845        ($lhs:expr, $rhs:expr) => {
846            BooleanExpr::And(Box::new($lhs), Box::new($rhs))
847        };
848    }
849
850    macro_rules! or {
851        ($lhs:expr, $rhs:expr) => {
852            BooleanExpr::Or(Box::new($lhs), Box::new($rhs))
853        };
854    }
855
856    macro_rules! not {
857        ($lhs:expr) => {
858            BooleanExpr::Not(Box::new($lhs))
859        };
860    }
861
862    macro_rules! var {
863        ($name:literal) => {
864            BooleanExpr::Var($name.to_string())
865        };
866    }
867
868    macro_rules! pattern {
869        ($regex:literal) => {
870            BooleanExpr::Pattern(Regex::new($regex).unwrap())
871        };
872    }
873
874    #[test]
875    fn test_script_boolean_expr_variables() {
876        let vars = BTreeSet::from([
877            "its-true",
878            "false-lol-true",
879            "under_score",
880            "e=quals",
881            "d1g1ts",
882        ]);
883
884        let expr = parse!("true", BooleanExpr::Lit(true));
885        assert_eval_true!(expr, vars);
886        let expr = parse!("false", BooleanExpr::Lit(false));
887        assert_eval_false!(expr, vars);
888        let expr = parse!("its-true", var!("its-true"));
889        assert_eval_true!(expr, vars);
890        let expr = parse!("under_score", var!("under_score"));
891        assert_eval_true!(expr, vars);
892        let expr = parse!("e=quals", var!("e=quals"));
893        assert_eval_true!(expr, vars);
894        let expr = parse!("d1g1ts", var!("d1g1ts"));
895        assert_eval_true!(expr, vars);
896        let expr = parse!("{{its.+}}", pattern!("(its.+)"));
897        assert_eval_true!(expr, vars);
898        let expr = parse!("{{false-[lo]+-true}}", pattern!("(false-[lo]+-true)"));
899        assert_eval_true!(expr, vars);
900        let expr = parse!(
901            "{{(true|false)-lol-(true|false)}}",
902            pattern!("((true|false)-lol-(true|false))")
903        );
904        assert_eval_true!(expr, vars);
905        let expr = parse!("d1g{{[0-9]}}ts", pattern!("d1g([0-9])ts"));
906        assert_eval_true!(expr, vars);
907        let expr = parse!("d1g{{[0-9]}}t{{[a-z]}}", pattern!("d1g([0-9])t([a-z])"));
908        assert_eval_true!(expr, vars);
909        let expr = parse!("d1{{(g|1)+}}ts", pattern!("d1((g|1)+)ts"));
910        assert_eval_true!(expr, vars);
911    }
912
913    #[test]
914    fn test_script_boolean_expr_operators_or() {
915        let vars = BTreeSet::default();
916
917        let expr = parse!(
918            "true || true",
919            or!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
920        );
921        assert_eval_true!(expr, vars);
922        let expr = parse!(
923            "true || false",
924            or!(BooleanExpr::Lit(true), BooleanExpr::Lit(false))
925        );
926        assert_eval_true!(expr, vars);
927        let expr = parse!(
928            "false || true",
929            or!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
930        );
931        assert_eval_true!(expr, vars);
932        let expr = parse!(
933            "false || false",
934            or!(BooleanExpr::Lit(false), BooleanExpr::Lit(false))
935        );
936        assert_eval_false!(expr, vars);
937    }
938
939    #[test]
940    fn test_script_boolean_expr_operators_and() {
941        let vars = BTreeSet::default();
942
943        let expr = parse!(
944            "true && true",
945            and!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
946        );
947        assert_eval_true!(expr, vars);
948        let expr = parse!(
949            "true && false",
950            and!(BooleanExpr::Lit(true), BooleanExpr::Lit(false))
951        );
952        assert_eval_false!(expr, vars);
953        let expr = parse!(
954            "false && true",
955            and!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
956        );
957        assert_eval_false!(expr, vars);
958        let expr = parse!(
959            "false && false",
960            and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false))
961        );
962        assert_eval_false!(expr, vars);
963    }
964
965    #[test]
966    fn test_script_boolean_expr_operators_not() {
967        let vars = BTreeSet::default();
968
969        let expr = parse!("!true", not!(BooleanExpr::Lit(true)));
970        assert_eval_false!(expr, vars);
971        let expr = parse!("!false", not!(BooleanExpr::Lit(false)));
972        assert_eval_true!(expr, vars);
973        let expr = parse!("!!false", not!(not!(BooleanExpr::Lit(false))));
974        assert_eval_false!(expr, vars);
975    }
976
977    #[test]
978    fn test_script_boolean_expr_operators_mixed() {
979        let vars = BTreeSet::default();
980
981        let expr = parse!("    ((!((false) ))    )  ", not!(BooleanExpr::Lit(false)));
982        assert_eval_true!(expr, vars);
983        let expr = parse!(
984            "true && (true && (true))",
985            and!(
986                BooleanExpr::Lit(true),
987                and!(BooleanExpr::Lit(true), BooleanExpr::Lit(true))
988            )
989        );
990        assert_eval_true!(expr, vars);
991        let expr = parse!(
992            "!false && !false && !! !false",
993            and!(
994                and!(not!(BooleanExpr::Lit(false)), not!(BooleanExpr::Lit(false))),
995                not!(not!(not!(BooleanExpr::Lit(false))))
996            )
997        );
998        assert_eval_true!(expr, vars);
999        let expr = parse!(
1000            "false && false || true",
1001            or!(
1002                and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false)),
1003                BooleanExpr::Lit(true)
1004            )
1005        );
1006        assert_eval_true!(expr, vars);
1007        let expr = parse!(
1008            "(false && false) || true",
1009            or!(
1010                and!(BooleanExpr::Lit(false), BooleanExpr::Lit(false)),
1011                BooleanExpr::Lit(true)
1012            )
1013        );
1014        assert_eval_true!(expr, vars);
1015        let expr = parse!(
1016            "false && (false || true)",
1017            and!(
1018                BooleanExpr::Lit(false),
1019                or!(BooleanExpr::Lit(false), BooleanExpr::Lit(true))
1020            )
1021        );
1022        assert_eval_false!(expr, vars);
1023    }
1024
1025    #[test]
1026    fn test_script_boolean_expr_evaluate() {
1027        let mut vars = BTreeSet::from(["linux", "target=x86_64-unknown-linux-gnu"]);
1028
1029        let expr = parse!(
1030            "linux && (target={{aarch64-.+}} || target={{x86_64-.+}})",
1031            and!(
1032                var!("linux"),
1033                or!(
1034                    pattern!("target=(aarch64-.+)"),
1035                    pattern!("target=(x86_64-.+)")
1036                )
1037            )
1038        );
1039        assert!(expr.evaluate(&vars));
1040        vars.clear();
1041        vars.extend(["linux", "target=i386-unknown-linux-gnu"]);
1042        assert!(!expr.evaluate(&vars));
1043
1044        let expr = parse!(
1045            "use_system_cxx_lib && target={{.+}}-apple-macosx10.{{9|10|11|12}} && !no-exceptions",
1046            and!(
1047                and!(
1048                    var!("use_system_cxx_lib"),
1049                    pattern!("target=(.+)\\-apple\\-macosx10\\.(9|10|11|12)")
1050                ),
1051                not!(var!("no-exceptions"))
1052            )
1053        );
1054        vars.clear();
1055        vars.extend(["use_system_cxx_lib", "target=arm64-apple-macosx10.12"]);
1056        assert!(expr.evaluate(&vars));
1057        vars.insert("no-exceptions");
1058        assert!(!expr.evaluate(&vars));
1059        vars.clear();
1060        vars.extend(["use_system_cxx_lib", "target=arm64-apple-macosx10.15"]);
1061        assert!(!expr.evaluate(&vars));
1062    }
1063
1064    #[test]
1065    #[should_panic(expected = "unexpected character: '#' is not valid here")]
1066    fn test_script_boolean_expr_invalid_ident() {
1067        assert_parse_error!("ba#d");
1068    }
1069
1070    #[test]
1071    #[should_panic(
1072        expected = "unexpected token: 'and' is not valid here\nhelp: expected one of: end of expression"
1073    )]
1074    fn test_script_boolean_expr_invalid_operator() {
1075        assert_parse_error!("true and true");
1076    }
1077
1078    #[test]
1079    #[should_panic(
1080        expected = "unexpected token: '||' is not valid here\nhelp: expected one of: '!', '(', '{{', identifier"
1081    )]
1082    fn test_script_boolean_expr_missing_lhs_operand() {
1083        assert_parse_error!("|| true");
1084    }
1085
1086    #[test]
1087    #[should_panic(
1088        expected = "unexpected end of expression: occurs here\nhelp: expected one of: '!', '(', '{{', identifier"
1089    )]
1090    fn test_script_boolean_expr_missing_rhs_operand() {
1091        assert_parse_error!("true &&");
1092    }
1093
1094    #[test]
1095    #[should_panic(
1096        expected = "unexpected end of expression: occurs here\nhelp: expected one of: '!', '(', '{{', identifier"
1097    )]
1098    fn test_script_boolean_expr_empty_input() {
1099        assert_parse_error!("");
1100    }
1101
1102    #[test]
1103    #[should_panic(
1104        expected = "unexpected end of expression: occurs here\nhelp: expected one of: ')'"
1105    )]
1106    fn test_script_boolean_expr_unclosed_group() {
1107        assert_parse_error!("(((true && true) || true)");
1108    }
1109
1110    #[test]
1111    #[should_panic(
1112        expected = "unexpected token: '(' is not valid here\nhelp: expected one of: end of expression"
1113    )]
1114    fn test_script_boolean_expr_ident_followed_by_group() {
1115        assert_parse_error!("true (true)");
1116    }
1117
1118    #[test]
1119    #[should_panic(
1120        expected = "unexpected token: ')' is not valid here\nhelp: expected one of: '!', '(', '{{', identifier"
1121    )]
1122    fn test_script_boolean_expr_empty_group() {
1123        assert_parse_error!("(  )");
1124    }
1125
1126    #[test]
1127    #[should_panic(
1128        expected = "unexpected end of expression: occurs here\nhelp: expected one of: '}'"
1129    )]
1130    fn test_script_boolean_expr_unclosed_pattern() {
1131        assert_parse_error!("abc{def");
1132    }
1133
1134    #[test]
1135    #[should_panic(
1136        expected = "unexpected end of expression: occurs here\nhelp: expected one of: '}'"
1137    )]
1138    fn test_script_boolean_expr_empty_pattern() {
1139        assert_parse_error!("{}");
1140    }
1141
1142    #[track_caller]
1143    #[inline(never)]
1144    fn assert_eval(expr: &BooleanExpr, vars: &BTreeSet<&'static str>, expected_result: bool) {
1145        let result = expr.evaluate(vars);
1146        assert_eq!(
1147            result, expected_result,
1148            "expected {expr} to evaluate to {expected_result} with environment: {vars:?}"
1149        );
1150    }
1151}