codegen_cfg/
parsing.rs

1use crate::ast::*;
2
3use std::backtrace::Backtrace;
4use std::ops::Not as _;
5use std::panic::Location;
6
7use rust_utils::default::default;
8
9#[derive(Debug)]
10pub struct Error<'p> {
11    pub input: &'p str,
12    pub location: &'static Location<'static>,
13    pub backtrace: Backtrace,
14}
15
16pub type Result<'p, T, E = Error<'p>> = std::result::Result<T, E>;
17
18macro_rules! ensure {
19    ($s:expr, $cond:expr) => {
20        if !$cond {
21            return Err(Error {
22                input: $s,
23                location: Location::caller(),
24                backtrace: Backtrace::capture(),
25            });
26        }
27    };
28}
29
30pub fn parse(mut s: &str) -> Result<Expr> {
31    let s = &mut s;
32    skip_space0(s);
33    let expr = parse_expr(s)?;
34    skip_space0(s);
35    ensure!(s, s.is_empty());
36    Ok(expr)
37}
38
39fn take_while1<'p>(s: &mut &'p str, f: impl Fn(char) -> bool) -> Result<'p, &'p str> {
40    let end = s
41        .char_indices()
42        .find_map(|(i, c)| f(c).not().then_some(i))
43        .unwrap_or(s.len());
44
45    ensure!(s, end > 0);
46
47    let (ans, rest) = s.split_at(end);
48    *s = rest;
49    Ok(ans)
50}
51
52fn consume_tag<'p>(s: &mut &'p str, tag: &str) -> Result<'p, &'p str> {
53    ensure!(s, s.starts_with(tag));
54    let (ans, rest) = s.split_at(tag.len());
55    *s = rest;
56    Ok(ans)
57}
58
59fn skip_space0(s: &mut &str) {
60    *s = s.trim_start();
61}
62
63fn skip_tag<'p>(s: &mut &'p str, tag: &str) -> Option<&'p str> {
64    if s.starts_with(tag) {
65        let (ans, rest) = s.split_at(tag.len());
66        *s = rest;
67        Some(ans)
68    } else {
69        None
70    }
71}
72
73// https://doc.rust-lang.org/reference/conditional-compilation.html
74
75fn parse_expr<'p>(s: &mut &'p str) -> Result<'p, Expr> {
76    if s.starts_with("any") {
77        Ok(expr(parse_any(s)?))
78    } else if s.starts_with("all") {
79        Ok(expr(parse_all(s)?))
80    } else if s.starts_with("not") {
81        Ok(expr(parse_not(s)?))
82    } else {
83        Ok(expr(parse_pred(s)?))
84    }
85}
86
87fn parse_pred<'p>(s: &mut &'p str) -> Result<'p, Pred> {
88    let key = parse_identifier(s)?;
89
90    let has_value = s.trim_start().starts_with('=');
91
92    let value = if has_value {
93        skip_space0(s);
94        skip_tag(s, "=");
95        skip_space0(s);
96        Some(parse_string_literal(s)?)
97    } else {
98        None
99    };
100
101    Ok(Pred {
102        key: key.into(),
103        value: value.map(Into::into),
104    })
105}
106
107// TODO: unicode identifier
108fn parse_identifier<'p>(s: &mut &'p str) -> Result<'p, &'p str> {
109    ensure!(s, s.starts_with(|c| matches!(c, 'a'..='z'|'A'..='Z'|'_')));
110    take_while1(s, |c| matches!(c, 'a'..='z'|'A'..='Z'|'0'..='9'|'_'))
111}
112
113// TODO: escaped string
114fn parse_string_literal<'p>(s: &mut &'p str) -> Result<'p, &'p str> {
115    consume_tag(s, "\"")?;
116
117    let ans = take_while1(s, |c| c != '"')?;
118    assert!(ans.contains('\\').not());
119
120    consume_tag(s, "\"")?;
121
122    Ok(ans)
123}
124
125fn parse_any<'p>(s: &mut &'p str) -> Result<'p, Any<Pred>> {
126    consume_tag(s, "any")?;
127    skip_space0(s);
128    consume_tag(s, "(")?;
129    let list = parse_expr_list(s)?;
130    skip_space0(s);
131    consume_tag(s, ")")?;
132    Ok(any(list))
133}
134
135fn parse_all<'p>(s: &mut &'p str) -> Result<'p, All<Pred>> {
136    consume_tag(s, "all")?;
137    skip_space0(s);
138    consume_tag(s, "(")?;
139    let list = parse_expr_list(s)?;
140    skip_space0(s);
141    consume_tag(s, ")")?;
142    Ok(all(list))
143}
144
145fn parse_not<'p>(s: &mut &'p str) -> Result<'p, Not<Pred>> {
146    consume_tag(s, "not")?;
147    skip_space0(s);
148    consume_tag(s, "(")?;
149    let expr = parse_expr(s)?;
150    skip_space0(s);
151    consume_tag(s, ")")?;
152    Ok(not(expr))
153}
154
155fn parse_expr_list<'p>(s: &mut &'p str) -> Result<'p, Vec<Expr>> {
156    let mut ans: Vec<Expr> = default();
157
158    while s.starts_with(')').not() {
159        skip_space0(s);
160        ans.push(parse_expr(s)?);
161        skip_space0(s);
162        skip_tag(s, ",");
163    }
164
165    Ok(ans)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn cfg_string() {
174        let input = r#"  all(not(any(target_os = "linux", target_os = "macos")), all(), any(unix))  "#;
175        let expr = parse(input).unwrap();
176        assert_eq!(expr.to_string(), input.trim());
177    }
178}