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#[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#[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(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 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(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 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#[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
489struct 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 (_, '}') 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}