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}