pomsky_macro/
lib.rs

1//! This crate provides the [`pomsky!`] macro to compile [pomsky] expressions at
2//! compile time.
3//!
4//! [pomsky]: https://pomsky-lang.org
5#![cfg_attr(feature = "diagnostics", feature(proc_macro_span))]
6
7extern crate proc_macro;
8
9use std::iter::Peekable;
10
11use proc_macro::{Delimiter, Group, Literal, Span, TokenStream, TokenTree};
12
13use pomsky::{
14    options::{CompileOptions, RegexFlavor},
15    Expr,
16};
17
18mod diagnostic;
19
20/// Macro to compile a [pomsky] expression at compile time.
21///
22/// ### Example
23///
24/// ```
25/// const REGEX: &str = pomsky! {
26///     let number = '-'? [digit]+;
27///     let op = ["+-*/"];
28///     number (op number)*
29/// };
30/// ```
31///
32/// **NOTE**: Code points (e.g. `U+FFEF`) should be written without the `+`
33/// (i.e., `UFFEF`), because rustfmt surrounds `+` with spaces by default, which
34/// would break parsing.
35///
36/// The regex flavor defaults to `Rust`, so it can be used with the [regex]
37/// crate:
38///
39/// ```rust,no_test
40/// use regex::Regex;
41///
42/// fn get_regex() -> Regex {
43///     Regex::new(pomsky! { ... }).unwrap()
44/// }
45/// ```
46///
47/// If you want to use a different flavor, you can specify it in the first line,
48/// like so:
49///
50/// ```rust,no_test
51/// pomsky! {
52///     #flavor = Pcre
53///     // your pomsky expression goes here
54/// }
55/// ```
56///
57/// Available flavors are
58///
59/// - **DotNet** (C#, F#)
60/// - **Java**
61/// - **JavaScript** (ECMAScript, Dart)
62/// - **Pcre** (Crystal, Delphi, Elixir, Erlang, Hack, Julia, PHP, R, Vala, ...)
63/// - **Python** (`re` module)
64/// - **Ruby**
65/// - **Rust** (`regex` crate)
66///
67/// [pomsky]: https://pomsky-lang.org
68/// [regex]: https://docs.rs/regex
69#[proc_macro]
70pub fn pomsky(items: TokenStream) -> TokenStream {
71    let group = Group::new(Delimiter::None, items);
72    let global_span = group.span();
73
74    match pomsky_impl(group.stream().into_iter()) {
75        Ok(lit) => TokenTree::Literal(lit).into(),
76        Err(Error { msg, span }) => {
77            let span = span.unwrap_or(global_span);
78            diagnostic::error(&msg, span, span)
79        }
80    }
81}
82
83struct Error {
84    msg: String,
85    span: Option<Span>,
86}
87
88impl Error {
89    fn new(msg: String, span: Span) -> Self {
90        Error { msg, span: Some(span) }
91    }
92
93    fn from_msg(msg: String) -> Self {
94        Error { msg, span: None }
95    }
96}
97
98macro_rules! bail {
99    ($l:literal) => {
100        return Err(Error::from_msg(format!($l)))
101    };
102    ($l:literal, $e:expr) => {
103        return Err(Error::new(format!($l), $e))
104    };
105    ($e1:expr) => {
106        return Err(Error::from_msg($e1))
107    };
108    ($e1:expr, $e2:expr) => {
109        return Err(Error::new($e1, $e2))
110    };
111}
112
113fn expect(
114    iter: &mut Peekable<impl Iterator<Item = TokenTree>>,
115    pred: fn(&TokenTree) -> bool,
116    error_msg: impl Into<String>,
117) -> Result<(), Error> {
118    match iter.peek() {
119        Some(tt) if pred(tt) => {
120            iter.next();
121            Ok(())
122        }
123        Some(tt) => bail!(error_msg.into(), tt.span()),
124        None => bail!(error_msg.into()),
125    }
126}
127
128fn pomsky_impl(items: impl Iterator<Item = TokenTree>) -> Result<Literal, Error> {
129    let mut iter = items.peekable();
130
131    let found_hashtag =
132        expect(&mut iter, |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '#'), "");
133
134    let flavor = if found_hashtag.is_ok() {
135        expect(
136            &mut iter,
137            |t| matches!(t, TokenTree::Ident(id) if &id.to_string() == "flavor"),
138            "expected `flavor`",
139        )?;
140        expect(
141            &mut iter,
142            |t| matches!(t, TokenTree::Punct(p) if p.as_char() == '='),
143            "expected `=`",
144        )?;
145
146        get_flavor(iter.next())?
147    } else {
148        RegexFlavor::Rust
149    };
150
151    let group = Group::new(Delimiter::None, iter.collect());
152
153    #[cfg(not(feature = "diagnostics"))]
154    let (span, input) = (group.span(), group.to_string());
155
156    #[cfg(feature = "diagnostics")]
157    let (span, input) = {
158        if let (Some(first), Some(last)) =
159            (group.stream().into_iter().next(), group.stream().into_iter().last())
160        {
161            let span = first.span().join(last.span()).unwrap();
162            (span, span.source_text().unwrap())
163        } else {
164            (group.span_close(), String::new())
165        }
166    };
167
168    let input = input.trim_start_matches("/*«*/").trim_end_matches("/*»*/");
169
170    match Expr::parse_and_compile(input, CompileOptions { flavor, ..Default::default() }) {
171        (Some(compiled), _warnings, _tests) => Ok(Literal::string(&compiled)),
172
173        (None, errors, _) => {
174            let errors =
175                errors.into_iter().map(|d| diagnostic::fmt(d, &group, input)).collect::<Vec<_>>();
176            bail!(errors.join("\n\n"), span)
177        }
178    }
179}
180
181fn get_flavor(item: Option<TokenTree>) -> Result<RegexFlavor, Error> {
182    Ok(match item {
183        Some(TokenTree::Ident(id)) => match id.to_string().as_str() {
184            "DotNet" => RegexFlavor::DotNet,
185            "Java" => RegexFlavor::Java,
186            "JavaScript" => RegexFlavor::JavaScript,
187            "Pcre" => RegexFlavor::Pcre,
188            "Python" => RegexFlavor::Python,
189            "Ruby" => RegexFlavor::Ruby,
190            "Rust" => RegexFlavor::Rust,
191            s => bail!(
192                "Expected one of: DotNet, Java, JavaScript, Pcre, Python, Ruby, Rust\nGot: {s}",
193                id.span()
194            ),
195        },
196        Some(tt) => bail!("Unexpected token `{tt}`", tt.span()),
197        None => bail!("Expected identifier"),
198    })
199}