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#[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#[derive(Debug, Clone)]
63pub enum BooleanExpr {
64 Lit(bool),
66 Var(String),
70 Pattern(Regex),
72 Not(Box<BooleanExpr>),
74 And(Box<BooleanExpr>, Box<BooleanExpr>),
76 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 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
213struct BooleanExprParser<'a> {
215 tokenizer: Tokenizer<'a>,
216 current: Span<Token<'a>>,
217 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 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#[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
483struct 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 (_, '}') 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}