wayver's git archive


an obsidian renderer
git clone https://git.wayver.dev/sable

sable-bases/src/filter/parser.rs@main

raw
Date Commit Message Author Files + -
2026-02-23 01:55 initial mvp wayverd 139 17808 0
...

1use std::borrow::Cow;
2
3use chumsky::{
4    input::MappedInput,
5    pratt::{infix, left, postfix, prefix},
6    prelude::*,
7};
8
9use super::ast::{BinOp, Expr, Ident, Token, UnOp};
10
11pub fn lexer<'src>()
12-> impl Parser<'src, &'src str, Vec<Spanned<Token<'src>>>, extra::Err<Rich<'src, char>>> {
13    recursive(|_token| {
14        choice((
15            text::ident().map(|s| match s {
16                "true" => Token::Bool(true),
17                "false" => Token::Bool(false),
18                s => Token::Ident(Ident(Cow::from(s))),
19            }),
20            just('"')
21                .ignore_then(none_of('"').repeated().to_slice())
22                .then_ignore(just('"'))
23                .map(|s| Token::String(Cow::from(s))),
24            just("+").to(Token::Add),
25            just("-").to(Token::Sub),
26            just("*").to(Token::Mul),
27            just("/").to(Token::Div),
28            just("%").to(Token::Mod),
29            just("==").to(Token::Equal),
30            just("!=").to(Token::NotEqual),
31            just(">").to(Token::GreaterThan),
32            just("<").to(Token::LessThan),
33            just(">=").to(Token::GreaterEqual),
34            just("<=").to(Token::LessEqual),
35            just("!").to(Token::Not),
36            just("&&").to(Token::And),
37            just("||").to(Token::Or),
38            just("(").to(Token::ParenOpen),
39            just(")").to(Token::ParenClose),
40            just("[").to(Token::BracketOpen),
41            just("]").to(Token::BracketClose),
42            just(",").to(Token::Comma),
43            just(".").to(Token::Period),
44            text::int(10)
45                .then(just('.').then(text::digits(10)).or_not())
46                .to_slice()
47                .from_str()
48                .unwrapped()
49                .map(Token::Decimal),
50            text::int(10)
51                .to_slice()
52                .from_str()
53                .unwrapped()
54                .map(Token::Integer),
55        ))
56        .spanned()
57        .padded()
58    })
59    .repeated()
60    .collect()
61}
62
63#[cfg(test)]
64mod test_lexer {
65    use chumsky::prelude::*;
66
67    use super::*;
68
69    #[rstest::rstest]
70    #[case(
71        r#"note["link-data"] == "fanfiction""#,
72        vec![
73            (Token::Ident(Ident("note".into())).with_span(SimpleSpan::from(0..4))),
74            (Token::BracketOpen.with_span(SimpleSpan::from(4..5))),
75            (Token::String("link-data".into()).with_span(SimpleSpan::from(5..16))),
76            (Token::BracketClose.with_span(SimpleSpan::from(16..17))),
77            (Token::Equal.with_span(SimpleSpan::from(18..20))),
78            (Token::String("fanfiction".into()).with_span(SimpleSpan::from(21..33))),
79        ],
80    )]
81    fn test(#[case] input: &str, #[case] expected: Vec<Spanned<Token<'_>>>) {
82        let tokens = lexer().parse(input);
83        assert!(!tokens.has_errors());
84        assert_eq!(tokens.into_output(), Some(expected));
85    }
86
87    #[test]
88    fn setup_test() {
89        let tokens = lexer().parse(r#"note["link-data"] == "fanfiction""#);
90        assert!(!tokens.has_errors());
91        assert_eq!(
92            tokens.into_output(),
93            Some(vec![
94                (Token::Ident(Ident("note".into())).with_span(SimpleSpan::from(0..4))),
95                (Token::BracketOpen.with_span(SimpleSpan::from(4..5))),
96                (Token::String("link-data".into()).with_span(SimpleSpan::from(5..16))),
97                (Token::BracketClose.with_span(SimpleSpan::from(16..17))),
98                (Token::Equal.with_span(SimpleSpan::from(18..20))),
99                (Token::String("fanfiction".into()).with_span(SimpleSpan::from(21..33))),
100            ])
101        );
102    }
103}
104
105pub fn parser<'tokens, 'src: 'tokens>() -> impl Parser<
106    'tokens,
107    MappedInput<'tokens, Token<'src>, SimpleSpan, &'tokens [Spanned<Token<'src>>]>,
108    Spanned<Expr<'src>>,
109    extra::Err<Rich<'tokens, Token<'src>>>,
110> {
111    recursive(|expr| {
112        let value = select! {
113            Token::Bool(b) => Expr::Bool(b),
114            Token::Integer(i) => Expr::Integer(i),
115            Token::Decimal(f) => Expr::Decimal(f),
116            Token::String(s) => Expr::String(s),
117        }
118        .labelled("value");
119
120        let ident = select! { Token::Ident(x) => x }.labelled("identifier");
121        let ident_expr = select! { Token::Ident(x) => Expr::Ident(x) }.labelled("identifier");
122
123        let params = expr
124            .clone()
125            .separated_by(just(Token::Comma))
126            .allow_trailing()
127            .collect::<Vec<_>>()
128            .delimited_by(just(Token::ParenOpen), just(Token::ParenClose))
129            .spanned();
130
131        let group = expr
132            .clone()
133            .delimited_by(just(Token::ParenOpen), just(Token::ParenClose))
134            .map(|inner| Expr::Group(Box::new(inner)))
135            .boxed();
136
137        let indexed = expr
138            .clone()
139            .delimited_by(just(Token::BracketOpen), just(Token::BracketClose));
140
141        choice((value, ident_expr, group)).spanned().pratt((
142            postfix(
143                16,
144                just(Token::Period)
145                    .ignore_then(ident.spanned())
146                    .then(params.clone()),
147                |name, (method, params), e| {
148                    Expr::Method(Box::new(name), method, params).with_span(e.span())
149                },
150            ),
151            postfix(
152                15,
153                just(Token::Period).ignore_then(ident.spanned()),
154                |name, method, e| Expr::Member(Box::new(name), method).with_span(e.span()),
155            ),
156            postfix(14, params.clone(), |name, params, e| {
157                Expr::Function(Box::new(name), params).with_span(e.span())
158            }),
159            postfix(13, indexed, |name, params, e| {
160                Expr::Index(Box::new(name), Box::new(params)).with_span(e.span())
161            }),
162            //
163            infix(
164                left(12),
165                select! { Token::Or = e => BinOp::Or.with_span(e.span()) },
166                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
167            ),
168            infix(
169                left(11),
170                select! { Token::And = e => BinOp::And.with_span(e.span()) },
171                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
172            ),
173            //
174            infix(
175                left(7),
176                select! { Token::Equal = e => BinOp::Equal.with_span(e.span()) },
177                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
178            ),
179            infix(
180                left(7),
181                select! { Token::NotEqual = e => BinOp::NotEqual.with_span(e.span()) },
182                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
183            ),
184            //
185            infix(
186                left(6),
187                select! { Token::GreaterThan = e => BinOp::GreaterThan.with_span(e.span()) },
188                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
189            ),
190            infix(
191                left(6),
192                select! { Token::GreaterEqual = e => BinOp::GreaterEqual.with_span(e.span()) },
193                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
194            ),
195            infix(
196                left(6),
197                select! { Token::LessThan = e => BinOp::LessThan.with_span(e.span()) },
198                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
199            ),
200            infix(
201                left(6),
202                select! { Token::LessEqual = e => BinOp::LessEqual.with_span(e.span()) },
203                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
204            ),
205            //
206            infix(
207                left(4),
208                select! { Token::Add = e => BinOp::Add.with_span(e.span()) },
209                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
210            ),
211            infix(
212                left(4),
213                select! { Token::Sub = e => BinOp::Sub.with_span(e.span()) },
214                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
215            ),
216            //
217            infix(
218                left(3),
219                select! { Token::Mul = e => BinOp::Mul.with_span(e.span()) },
220                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
221            ),
222            infix(
223                left(3),
224                select! { Token::Div = e => BinOp::Div.with_span(e.span()) },
225                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
226            ),
227            infix(
228                left(3),
229                select! { Token::Mod = e => BinOp::Mod.with_span(e.span()) },
230                |x, op, y, e| Expr::BinOp(op, Box::new(x), Box::new(y)).with_span(e.span()),
231            ),
232            //
233            prefix(
234                2,
235                select! { Token::Sub = e => UnOp::Neg.with_span(e.span()) },
236                |op, x, e| Expr::UnOp(op, Box::new(x)).with_span(e.span()),
237            ),
238            prefix(
239                2,
240                select! { Token::Not = e => UnOp::Not.with_span(e.span()) },
241                |op, x, e| Expr::UnOp(op, Box::new(x)).with_span(e.span()),
242            ),
243        ))
244    })
245}
246
247#[cfg(test)]
248mod test_parser {
249    use chumsky::prelude::*;
250
251    use super::*;
252
253    #[test]
254    fn setup_test() {
255        let tokens = lexer().parse(r#"note["link-data"] == "fanfiction""#);
256        assert!(!tokens.has_errors());
257        assert_eq!(
258            tokens.into_output(),
259            Some(vec![
260                (Token::Ident(Ident("note".into())).with_span(SimpleSpan::from(0..4))),
261                (Token::BracketOpen.with_span(SimpleSpan::from(4..5))),
262                (Token::String("link-data".into()).with_span(SimpleSpan::from(5..16))),
263                (Token::BracketClose.with_span(SimpleSpan::from(16..17))),
264                (Token::Equal.with_span(SimpleSpan::from(18..20))),
265                (Token::String("fanfiction".into()).with_span(SimpleSpan::from(21..33))),
266            ])
267        );
268    }
269}
270