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