Skip to main content

microcad_lang_parse/parser/
parsers.rs

1// Copyright © 2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Chumsky parser functions for µcad syntax elements
5
6use crate::ast;
7
8use crate::parser::{ParserInput, RichError, helpers::ParserExt};
9use crate::tokens::Token;
10use chumsky::extra::Full;
11use chumsky::inspector::Inspector;
12use chumsky::{IterParser, Parser, select_ref};
13
14/// Alias for parser input type
15pub type PInput<'a> = ParserInput<'a, 'a>;
16
17/// Alias for parser error type
18pub type PError<'a, S, Ctx> = Full<RichError<'a>, S, Ctx>;
19
20/// Alias for Inspector type (as a trait bound shorthand)
21pub trait PInspector<'a>: Inspector<'a, PInput<'a>> + Default + Clone + 'static {}
22
23impl<'a, T> PInspector<'a> for T where T: Inspector<'a, PInput<'a>> + Default + Clone + 'static {}
24
25/// Unit parser.
26pub fn unit<'tokens, S, Ctx>()
27-> impl Parser<'tokens, PInput<'tokens>, ast::Unit, PError<'tokens, S, Ctx>>
28where
29    S: PInspector<'tokens>,
30    Ctx: 'tokens,
31{
32    select_ref! {
33        Token::Identifier(ident) = e => ast::Unit {
34            span: e.span(),
35            name: ident.as_ref().into()
36        },
37        Token::Unit(unit) = e => ast::Unit {
38            span: e.span(),
39            name: unit.as_ref().into()
40        },
41        Token::SigilQuote = e => ast::Unit {
42            span: e.span(),
43            name: r#"""#.into()
44        },
45    }
46    .labelled("unit")
47}
48
49/// Literal parser.
50pub fn literal<'tokens, S, Ctx>()
51-> impl Parser<'tokens, PInput<'tokens>, ast::Literal, PError<'tokens, S, Ctx>>
52where
53    S: PInspector<'tokens>,
54    Ctx: 'tokens,
55{
56    use microcad_lang_base::ToCompactString;
57    use std::str::FromStr;
58
59    {
60        let single_value = select_ref! {
61            Token::LiteralFloat(x) = e => {
62                match f64::from_str(x) {
63                    Ok(value) => ast::LiteralKind::Float(ast::FloatLiteral {
64                        value,
65                        raw: x.to_compact_string(),
66                        span: e.span(),
67                    }),
68                    Err(err) => ast::LiteralKind::Error(ast::LiteralError {
69                        span: e.span(),
70                        kind: err.into(),
71                    })
72                }
73            },
74            Token::LiteralInt(x) = e => {
75                match i64::from_str(x) {
76                    Ok(value) => ast::LiteralKind::Integer(ast::IntegerLiteral {
77                    value,
78                    raw: x.to_compact_string(),
79                    span: e.span(),
80                }),
81                    Err(err) => ast::LiteralKind::Error(ast::LiteralError {
82                        span: e.span(),
83                        kind: err.into(),
84                    })
85                }
86            },
87            Token::LiteralString(content) = e => {
88                ast::LiteralKind::String(ast::StringLiteral {
89                    span: e.span(),
90                    content: content.as_ref().into(),
91                })
92            },
93            Token::LiteralBool(value) = e => {
94                ast::LiteralKind::Bool(ast::BoolLiteral {
95                    span: e.span(),
96                    value: *value,
97                })
98            },
99        }
100        .boxed();
101
102        single_value
103            .then(unit().or_not())
104            .with_extras()
105            .try_map_with(|((literal, ty), extras), e| {
106                let literal = match (literal, ty) {
107                    (ast::LiteralKind::Float(float), Some(unit)) => {
108                        ast::LiteralKind::Quantity(ast::QuantityLiteral {
109                            span: e.span(),
110                            value: float.value,
111                            raw: float.raw,
112                            unit,
113                        })
114                    }
115                    (ast::LiteralKind::Integer(int), Some(unit)) => {
116                        ast::LiteralKind::Quantity(ast::QuantityLiteral {
117                            span: e.span(),
118                            value: int.value as f64,
119                            raw: int.raw,
120                            unit,
121                        })
122                    }
123                    (_, Some(_)) => ast::LiteralKind::Error(ast::LiteralError {
124                        span: e.span(),
125                        kind: ast::LiteralErrorKind::Untypable,
126                    }),
127                    (literal, None) => literal,
128                };
129                Ok(ast::Literal {
130                    span: e.span(),
131                    literal,
132                    extras,
133                })
134            })
135            .labelled("literal")
136            .boxed()
137    }
138}
139
140/// Comment parser.
141pub fn comment<'tokens, S, Ctx>()
142-> impl Parser<'tokens, PInput<'tokens>, ast::Comment, PError<'tokens, S, Ctx>>
143where
144    S: PInspector<'tokens>,
145    Ctx: 'tokens,
146{
147    let single_line_comments = select_ref! {
148        Token::SingleLineComment(comment) => comment
149    }
150    .map_with(|line, e| ast::Comment {
151        span: e.span(),
152        inner: ast::CommentInner::SingleLine(line.to_string()),
153    })
154    .boxed();
155    let multi_line = select_ref! {
156        Token::MultiLineComment(comment) = e => ast::Comment {
157            span: e.span(),
158            inner: ast::CommentInner::MultiLine(comment.to_string())
159        }
160    };
161
162    single_line_comments
163        .or(multi_line)
164        .labelled("comment")
165        .boxed()
166}
167
168/// Whitespace parser.
169pub fn whitespace<'tokens, S, Ctx>()
170-> impl Parser<'tokens, PInput<'tokens>, String, PError<'tokens, S, Ctx>> + 'tokens + Clone
171where
172    S: PInspector<'tokens>,
173    Ctx: 'tokens,
174{
175    select_ref! {
176        Token::Whitespace(s) => s.to_string(),
177    }
178    .labelled("whitespace")
179    .boxed()
180}
181
182/// Leading extras parser.
183pub fn leading_extras<'tokens, S, Ctx>()
184-> impl Parser<'tokens, PInput<'tokens>, ast::LeadingExtras, PError<'tokens, S, Ctx>>
185where
186    S: PInspector<'tokens>,
187    Ctx: 'tokens,
188{
189    // Inline the whitespace logic and the comment logic
190    let whitespace =
191        select_ref! { Token::Whitespace(s) => ast::ItemExtra::Whitespace(s.to_string()) };
192    let comment = comment().map(ast::ItemExtra::Comment);
193
194    comment
195        .or(whitespace)
196        .repeated()
197        .collect::<Vec<_>>()
198        .map(ast::LeadingExtras)
199        .boxed()
200}
201
202/// Trailing extras parser.
203pub fn trailing_extras<'tokens, S, Ctx>()
204-> impl Parser<'tokens, PInput<'tokens>, ast::TrailingExtras, PError<'tokens, S, Ctx>>
205where
206    S: PInspector<'tokens>,
207    Ctx: 'tokens,
208{
209    // Inline the whitespace logic and the comment logic
210    let whitespace =
211        select_ref! { Token::Whitespace(s) => ast::ItemExtra::Whitespace(s.to_string()) };
212    let comment = comment().map(ast::ItemExtra::Comment);
213
214    whitespace
215        .or(comment)
216        .repeated()
217        .collect::<Vec<_>>()
218        .map(ast::TrailingExtras)
219        .boxed()
220}