Skip to main content

candela/
macros.rs

1//! Macros: source regions an embedder expands.
2//!
3//! A macro invocation is written `name!( ... )`. Everything between the
4//! parentheses is a raw region that candela does not interpret. The region is
5//! handed to the expander registered under `name`, and the candela source the
6//! expander returns is parsed in its place, as one expression.
7//!
8//! candela never learns what a macro means. It finds where a region starts and
9//! ends, calls out, and parses what comes back. The meaning belongs to the host
10//! that registers the expander.
11//!
12//! ```no_run
13//! use candela::macros::MacroError;
14//!
15//! let mut engine = candela::Engine::new();
16//! engine.register_macro("shout", |body: &str| {
17//!     Ok::<String, MacroError>(format!("\"{}!\"", body.trim().to_uppercase()))
18//! });
19//! ```
20//!
21//! [`scan_regions`] exposes the same region scanner the lexer uses, so a build
22//! tool can find every invocation of one macro in a file without compiling it.
23
24use crate::cold_path;
25use rustc_hash::FxHashMap;
26use smol_strc::SmolStr;
27use std::cell::Cell;
28use std::cell::RefCell;
29use std::ops::Range;
30use std::rc::Rc;
31
32/// Why an expander refused the region it was handed.
33///
34/// `offset` is a byte offset into the region body. When it is set, the error is
35/// reported at that position in the file the macro was written in; otherwise it
36/// is reported against the whole invocation.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct MacroError {
39    pub message: String,
40    pub offset: Option<usize>,
41}
42
43impl MacroError {
44    /// An error covering the whole macro invocation.
45    #[must_use]
46    pub fn new(message: impl Into<String>) -> Self {
47        Self {
48            message: message.into(),
49            offset: None,
50        }
51    }
52
53    /// An error at `offset` bytes into the region body.
54    #[must_use]
55    pub fn at(message: impl Into<String>, offset: usize) -> Self {
56        Self {
57            message: message.into(),
58            offset: Some(offset),
59        }
60    }
61}
62
63/// One `name!( ... )` invocation found by [`scan_regions`].
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Region<'a> {
66    /// The raw text between the parentheses.
67    pub body: &'a str,
68    /// Byte offset of `body` in the scanned source.
69    pub body_start: usize,
70    /// Byte range of the whole invocation, from the first byte of the name
71    /// through the closing parenthesis.
72    pub span: Range<usize>,
73}
74
75/// Finds every `name!( ... )` invocation in `src`, in source order.
76///
77/// The scan is the one the lexer runs: a region ends at the parenthesis that
78/// balances the one that opened it, parentheses inside a string literal or a
79/// `//` comment do not count, and a `name!(` written inside a string literal or
80/// a comment is not an invocation. Scanning stops at a region that is never
81/// closed, so every region returned is complete.
82#[must_use]
83pub fn scan_regions<'a>(src: &'a str, name: &str) -> Vec<Region<'a>> {
84    let bytes = src.as_bytes();
85    let mut regions = Vec::new();
86    let mut i = 0;
87    while i < bytes.len() {
88        let b = bytes[i];
89        if b == b'"' {
90            let Some(after) = skip_string(bytes, i) else {
91                break;
92            };
93            i = after;
94        } else if b == b'/' && bytes.get(i + 1) == Some(&b'/') {
95            i = skip_line_comment(bytes, i);
96        } else if is_ident_start(b) {
97            let start = i;
98            let mut end = i + 1;
99            while end < bytes.len() && is_ident_continue(bytes[end]) {
100                end += 1;
101            }
102            if &src[start..end] == name
103                && bytes.get(end) == Some(&b'!')
104                && bytes.get(end + 1) == Some(&b'(')
105            {
106                let body_start = end + 2;
107                let Some(len) = region_len(&src[body_start..]) else {
108                    break;
109                };
110                let body_end = body_start + len;
111                regions.push(Region {
112                    body: &src[body_start..body_end],
113                    body_start,
114                    span: start..body_end + 1,
115                });
116                i = body_end + 1;
117            } else {
118                i = end;
119            }
120        } else {
121            i += 1;
122        }
123    }
124    regions
125}
126
127/// Length of the region body that begins at the start of `rest`, which is the
128/// text just past an opening `(`. `None` when the region is never closed.
129pub(crate) fn region_len(rest: &str) -> Option<usize> {
130    let bytes = rest.as_bytes();
131    let mut depth = 1usize;
132    let mut i = 0;
133    while i < bytes.len() {
134        match bytes[i] {
135            b'"' => i = skip_string(bytes, i)?,
136            b'/' if bytes.get(i + 1) == Some(&b'/') => i = skip_line_comment(bytes, i),
137            b'(' => {
138                depth += 1;
139                i += 1;
140            }
141            b')' => {
142                depth -= 1;
143                if depth == 0 {
144                    return Some(i);
145                }
146                i += 1;
147            }
148            _ => i += 1,
149        }
150    }
151    None
152}
153
154/// Byte index just past the string literal that opens at `start`. `None` when
155/// the literal is never closed.
156const fn skip_string(bytes: &[u8], start: usize) -> Option<usize> {
157    let mut i = start + 1;
158    while i < bytes.len() {
159        match bytes[i] {
160            b'\\' => i += 2,
161            b'"' => return Some(i + 1),
162            _ => i += 1,
163        }
164    }
165    None
166}
167
168/// Byte index of the line ending that ends the `//` comment at `start`, or the
169/// end of the input. Both `\n` and `\r` end a comment, as they do for the
170/// lexer.
171const fn skip_line_comment(bytes: &[u8], start: usize) -> usize {
172    let mut i = start + 2;
173    while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\r' {
174        i += 1;
175    }
176    i
177}
178
179const fn is_ident_start(b: u8) -> bool {
180    b.is_ascii_alphabetic() || b == b'_'
181}
182
183const fn is_ident_continue(b: u8) -> bool {
184    b.is_ascii_alphanumeric() || b == b'_'
185}
186
187type Expander = dyn Fn(&str) -> Result<String, MacroError>;
188
189/// The macros a compilation may use, and what an unregistered one does.
190///
191/// [`crate::Engine`] holds one of these and fills it through
192/// [`crate::Engine::register_macro`]. A tool that compiles without the
193/// embedder's expanders builds one directly, turns [`MacroEnv::allow_unknown`]
194/// on, and compiles inside [`MacroEnv::scope`].
195#[derive(Clone, Default)]
196pub struct MacroEnv {
197    expanders: FxHashMap<SmolStr, Rc<Expander>>,
198    allow_unknown: bool,
199}
200
201impl MacroEnv {
202    #[must_use]
203    pub fn new() -> Self {
204        Self::default()
205    }
206
207    /// Registers `expander` as the meaning of `name!( ... )`.
208    ///
209    /// The expander receives the raw region body and returns candela source for
210    /// one expression, or a [`MacroError`] that becomes a compile error at the
211    /// macro site. Registering a name twice keeps the last expander.
212    pub fn register<F>(&mut self, name: &str, expander: F)
213    where
214        F: Fn(&str) -> Result<String, MacroError> + 'static,
215    {
216        self.expanders.insert(SmolStr::new(name), Rc::new(expander));
217    }
218
219    /// Sets what an unregistered macro does: a compile error naming it (the
220    /// default), or, when `allow` is true, a `null` expression.
221    ///
222    /// Permitting unknown macros is for tooling that reads source it cannot
223    /// expand, such as the language server, which has no access to the host's
224    /// expanders and must not report the host's macros as errors.
225    pub const fn allow_unknown(&mut self, allow: bool) {
226        self.allow_unknown = allow;
227    }
228
229    #[must_use]
230    pub fn is_registered(&self, name: &str) -> bool {
231        self.expanders.contains_key(name)
232    }
233
234    /// Runs `f` with this environment active, and returns what it returns.
235    ///
236    /// Anything compiled inside `f` expands its macros through this
237    /// environment, including the files it imports. The previous environment is
238    /// restored afterwards, including when `f` unwinds.
239    pub fn scope<R>(&self, f: impl FnOnce() -> R) -> R {
240        let _active = Active::push(self.clone());
241        f()
242    }
243}
244
245thread_local! {
246    /// The environments installed by the [`MacroEnv::scope`] calls currently on
247    /// the stack; the last one is the active one.
248    static ACTIVE: RefCell<Vec<MacroEnv>> = const { RefCell::new(Vec::new()) };
249}
250
251/// Pops the environment it pushed when it is dropped, so a compile that unwinds
252/// leaves the stack as it found it.
253struct Active;
254
255impl Active {
256    fn push(env: MacroEnv) -> Self {
257        ACTIVE.with(|stack| stack.borrow_mut().push(env));
258        Self
259    }
260}
261
262impl Drop for Active {
263    fn drop(&mut self) {
264        ACTIVE.with(|stack| {
265            stack.borrow_mut().pop();
266        });
267    }
268}
269
270/// How far one macro may expand into another. An expansion is candela source
271/// like any other, so it may use a macro itself; a macro that expands to a use
272/// of itself would do that forever. Nesting this deep is past anything a host
273/// generates, and short of the stack the compiler runs on.
274pub(crate) const MAX_EXPANSION_DEPTH: u32 = 32;
275
276thread_local! {
277    /// How many expansions are being parsed right now.
278    static DEPTH: Cell<u32> = const { Cell::new(0) };
279}
280
281/// One level of macro expansion, held while the source it produced is parsed.
282/// The level is given back when the guard drops, including when the compile
283/// unwinds.
284pub(crate) struct Depth;
285
286impl Depth {
287    /// Takes a level, or nothing when [`MAX_EXPANSION_DEPTH`] are already held.
288    pub(crate) fn enter() -> Option<Self> {
289        DEPTH.with(|depth| {
290            let held = depth.get();
291            if held >= MAX_EXPANSION_DEPTH {
292                cold_path();
293                return None;
294            }
295            depth.set(held + 1);
296            Some(Self)
297        })
298    }
299}
300
301impl Drop for Depth {
302    fn drop(&mut self) {
303        DEPTH.with(|depth| depth.set(depth.get() - 1));
304    }
305}
306
307/// What the parser does with a macro region.
308pub(crate) enum Expansion {
309    /// Parse this source in place of the invocation.
310    Text(String),
311    /// No expander, and the active environment allows that: the invocation is a
312    /// `null` expression.
313    Null,
314    /// No expander, and an unregistered macro is an error.
315    Unknown,
316    /// The expander refused.
317    Failed(MacroError),
318}
319
320/// Expands `body` through the expander registered for `name` in the active
321/// environment.
322pub(crate) fn expand(name: &str, body: &str) -> Expansion {
323    // The expander runs outside the borrow: it may compile candela itself, and
324    // that pushes another environment onto this same stack.
325    let active = ACTIVE.with(|stack| {
326        stack
327            .borrow()
328            .last()
329            .map(|env| (env.expanders.get(name).cloned(), env.allow_unknown))
330    });
331    match active {
332        Some((Some(expander), _)) => match expander(body) {
333            Ok(source) => Expansion::Text(source),
334            Err(error) => Expansion::Failed(error),
335        },
336        Some((None, true)) => Expansion::Null,
337        _ => Expansion::Unknown,
338    }
339}